@agent-native/core 0.45.1 → 0.47.1
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.
- package/README.md +1 -0
- package/dist/agent/production-agent.d.ts +28 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +14 -7
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/cli/skills.d.ts +2 -2
- package/dist/cli/skills.d.ts.map +1 -1
- package/dist/cli/skills.js +33 -0
- package/dist/cli/skills.js.map +1 -1
- package/dist/client/components/LiveCursorOverlay.d.ts +46 -0
- package/dist/client/components/LiveCursorOverlay.d.ts.map +1 -0
- package/dist/client/components/LiveCursorOverlay.js +137 -0
- package/dist/client/components/LiveCursorOverlay.js.map +1 -0
- package/dist/client/components/PresenceBar.d.ts +11 -1
- package/dist/client/components/PresenceBar.d.ts.map +1 -1
- package/dist/client/components/PresenceBar.js +39 -7
- package/dist/client/components/PresenceBar.js.map +1 -1
- package/dist/client/components/RemoteSelectionRings.d.ts +43 -0
- package/dist/client/components/RemoteSelectionRings.d.ts.map +1 -0
- package/dist/client/components/RemoteSelectionRings.js +116 -0
- package/dist/client/components/RemoteSelectionRings.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -1
- package/dist/coding-tools/run-code.d.ts +40 -0
- package/dist/coding-tools/run-code.d.ts.map +1 -0
- package/dist/coding-tools/run-code.js +511 -0
- package/dist/coding-tools/run-code.js.map +1 -0
- package/dist/collab/awareness.d.ts +25 -0
- package/dist/collab/awareness.d.ts.map +1 -1
- package/dist/collab/awareness.js +42 -5
- package/dist/collab/awareness.js.map +1 -1
- package/dist/collab/client.d.ts +19 -1
- package/dist/collab/client.d.ts.map +1 -1
- package/dist/collab/client.js +362 -57
- package/dist/collab/client.js.map +1 -1
- package/dist/collab/follow-mode.d.ts +56 -0
- package/dist/collab/follow-mode.d.ts.map +1 -0
- package/dist/collab/follow-mode.js +54 -0
- package/dist/collab/follow-mode.js.map +1 -0
- package/dist/collab/index.d.ts +3 -1
- package/dist/collab/index.d.ts.map +1 -1
- package/dist/collab/index.js +5 -1
- package/dist/collab/index.js.map +1 -1
- package/dist/collab/presence.d.ts +56 -0
- package/dist/collab/presence.d.ts.map +1 -0
- package/dist/collab/presence.js +98 -0
- package/dist/collab/presence.js.map +1 -0
- package/dist/collab/routes.d.ts.map +1 -1
- package/dist/collab/routes.js +33 -6
- package/dist/collab/routes.js.map +1 -1
- package/dist/collab/struct-routes.d.ts.map +1 -1
- package/dist/collab/struct-routes.js +24 -4
- package/dist/collab/struct-routes.js.map +1 -1
- package/dist/collab/ydoc-manager.d.ts +13 -0
- package/dist/collab/ydoc-manager.d.ts.map +1 -1
- package/dist/collab/ydoc-manager.js +51 -15
- package/dist/collab/ydoc-manager.js.map +1 -1
- package/dist/extensions/fetch-tool.d.ts.map +1 -1
- package/dist/extensions/fetch-tool.js +62 -7
- package/dist/extensions/fetch-tool.js.map +1 -1
- package/dist/extensions/web-search-tool.d.ts +41 -0
- package/dist/extensions/web-search-tool.d.ts.map +1 -0
- package/dist/extensions/web-search-tool.js +200 -0
- package/dist/extensions/web-search-tool.js.map +1 -0
- package/dist/provider-api/custom-registry.d.ts +92 -0
- package/dist/provider-api/custom-registry.d.ts.map +1 -0
- package/dist/provider-api/custom-registry.js +289 -0
- package/dist/provider-api/custom-registry.js.map +1 -0
- package/dist/provider-api/index.d.ts +80 -44
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +569 -18
- package/dist/provider-api/index.js.map +1 -1
- package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
- package/dist/secrets/register-framework-secrets.js +36 -3
- package/dist/secrets/register-framework-secrets.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts +36 -0
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +119 -0
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/collab-plugin.d.ts +6 -0
- package/dist/server/collab-plugin.d.ts.map +1 -1
- package/dist/server/collab-plugin.js +105 -5
- package/dist/server/collab-plugin.js.map +1 -1
- package/dist/server/poll-events.d.ts +5 -0
- package/dist/server/poll-events.d.ts.map +1 -1
- package/dist/server/poll-events.js +27 -4
- package/dist/server/poll-events.js.map +1 -1
- package/dist/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/dist/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/dist/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/dist/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/dist/workspace-files/index.d.ts +4 -0
- package/dist/workspace-files/index.d.ts.map +1 -0
- package/dist/workspace-files/index.js +4 -0
- package/dist/workspace-files/index.js.map +1 -0
- package/dist/workspace-files/schema.d.ts +195 -0
- package/dist/workspace-files/schema.d.ts.map +1 -0
- package/dist/workspace-files/schema.js +48 -0
- package/dist/workspace-files/schema.js.map +1 -0
- package/dist/workspace-files/store.d.ts +89 -0
- package/dist/workspace-files/store.d.ts.map +1 -0
- package/dist/workspace-files/store.js +298 -0
- package/dist/workspace-files/store.js.map +1 -0
- package/dist/workspace-files/tool.d.ts +15 -0
- package/dist/workspace-files/tool.d.ts.map +1 -0
- package/dist/workspace-files/tool.js +226 -0
- package/dist/workspace-files/tool.js.map +1 -0
- package/docs/content/real-time-collaboration.md +481 -97
- package/package.json +2 -1
- package/src/templates/default/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/src/templates/default/.agents/skills/real-time-sync/SKILL.md +12 -2
- package/src/templates/workspace-core/.agents/skills/real-time-collab/SKILL.md +185 -37
- package/src/templates/workspace-core/.agents/skills/real-time-sync/SKILL.md +12 -2
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: real-time-collab
|
|
3
3
|
description: >-
|
|
4
|
-
Multi-user collaborative editing with Yjs CRDT
|
|
5
|
-
adding real-time collaborative editing to
|
|
6
|
-
or understanding how the agent and humans
|
|
4
|
+
Multi-user collaborative editing with Yjs CRDT, SSE fast-path transport, and
|
|
5
|
+
granular server-side merge. Use when adding real-time collaborative editing to
|
|
6
|
+
a template, debugging sync issues, or understanding how the agent and humans
|
|
7
|
+
edit documents simultaneously.
|
|
7
8
|
metadata:
|
|
8
9
|
internal: true
|
|
9
10
|
---
|
|
@@ -12,39 +13,57 @@ metadata:
|
|
|
12
13
|
|
|
13
14
|
## Rule
|
|
14
15
|
|
|
15
|
-
Collaborative editing uses Yjs CRDT via TipTap. The agent and human users are
|
|
16
|
+
Collaborative editing uses Yjs CRDT via TipTap. The agent and human users are
|
|
17
|
+
equal participants — both edit the same Y.Doc and changes merge cleanly without
|
|
18
|
+
conflicts. Always set `resourceType` on `createCollabPlugin`.
|
|
16
19
|
|
|
17
20
|
## How It Works
|
|
18
21
|
|
|
19
22
|
- **`Y.Doc`** stores the document as a `Y.XmlFragment` (ProseMirror node tree)
|
|
20
|
-
- **TipTap's Collaboration extension** binds the editor to the Y.XmlFragment
|
|
21
|
-
|
|
22
|
-
- **
|
|
23
|
-
|
|
23
|
+
- **TipTap's Collaboration extension** binds the editor to the Y.XmlFragment
|
|
24
|
+
via `ySyncPlugin`
|
|
25
|
+
- **CollaborationCaret extension** renders remote users' cursors with names and
|
|
26
|
+
colors
|
|
27
|
+
- **SSE fast-path** — `/_agent-native/poll-events` `EventSource` delivers collab
|
|
28
|
+
events push-style; while SSE is healthy the collab poll interval relaxes to
|
|
29
|
+
~12 s
|
|
30
|
+
- **Polling fallback** — `/_agent-native/poll` is polled every 2 s when SSE is
|
|
31
|
+
unavailable; this is the universal serverless fallback
|
|
32
|
+
- **Update batching** — local Yjs updates are debounced ~80 ms and coalesced
|
|
33
|
+
with `Y.mergeUpdates` before sending; flushed immediately on
|
|
34
|
+
`visibilitychange` / `pagehide`
|
|
35
|
+
- **SQL `_collab_docs` table** persists Yjs state as base64 (SQLite/Postgres
|
|
36
|
+
compatible). Tombstone compaction fires automatically when the stored blob
|
|
37
|
+
exceeds 4× the fresh encoded size.
|
|
24
38
|
|
|
25
39
|
## Agent + Human Editing
|
|
26
40
|
|
|
27
41
|
1. **Human edits** → TipTap → ySyncPlugin → Y.XmlFragment → `POST /_agent-native/collab/:docId/update`
|
|
28
42
|
2. **Agent edits** → action edits canonical SQL content + bumps `updatedAt` → change-sync refetch → the open editor reconciles the new content into the live Y.Doc (see below) → poll update → all clients
|
|
29
43
|
|
|
30
|
-
Both produce Yjs operations that merge cleanly. Agent edits appear without
|
|
44
|
+
Both produce Yjs operations that merge cleanly. Agent edits appear without
|
|
45
|
+
destroying cursor position, selection, or undo history.
|
|
31
46
|
|
|
32
|
-
|
|
47
|
+
The agent does **not** push edits into Yjs in-process and does **not** call any
|
|
48
|
+
localhost probe — those approaches silently no-op on serverless (the action runs
|
|
49
|
+
in a different process). The peer-editor model below replaced them.
|
|
33
50
|
|
|
34
51
|
## Agent Edits As A Real-Time Peer Editor
|
|
35
52
|
|
|
36
|
-
|
|
53
|
+
**SQL is the durable source of truth for document body content.** The agent
|
|
54
|
+
action edits the canonical content column and bumps `updatedAt`. No localhost
|
|
55
|
+
calls, no in-process Yjs mutation.
|
|
37
56
|
|
|
38
|
-
**
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
**The open editor reconciles authoritative external content into the live
|
|
58
|
+
Y.Doc.** The `updatedAt` bump flows through change-sync, which refetches the
|
|
59
|
+
record. The lead client applies the new content via `setContent`, producing Yjs
|
|
60
|
+
operations that merge with concurrent human edits. Every connected client
|
|
61
|
+
receives the result through normal Yjs sync.
|
|
41
62
|
|
|
42
63
|
### The `updatedAt` gate
|
|
43
64
|
|
|
44
|
-
The editor only adopts content that is genuinely **newer** than what it already reflects. An older-or-equal `updatedAt` is a lagging poll or a stale snapshot and is **ignored**.
|
|
45
|
-
|
|
46
65
|
```ts
|
|
47
|
-
//
|
|
66
|
+
// In the editor's reconcile effect
|
|
48
67
|
if (loaded.updatedAt > lastAppliedUpdatedAt.current) {
|
|
49
68
|
applyAuthoritativeContent(loaded.content); // adopt
|
|
50
69
|
lastAppliedUpdatedAt.current = loaded.updatedAt;
|
|
@@ -52,56 +71,107 @@ if (loaded.updatedAt > lastAppliedUpdatedAt.current) {
|
|
|
52
71
|
// else: lagging poll / stale snapshot → ignore
|
|
53
72
|
```
|
|
54
73
|
|
|
55
|
-
|
|
74
|
+
Without the gate, a slightly-behind poll response re-applies old content and
|
|
75
|
+
the edit "reverts on next poll". A fresh mount always adopts whatever content
|
|
76
|
+
it loaded.
|
|
56
77
|
|
|
57
78
|
### Lead-client election
|
|
58
79
|
|
|
59
|
-
Exactly ONE connected client applies
|
|
80
|
+
Exactly ONE connected client applies the authoritative snapshot; the rest
|
|
81
|
+
receive it through Yjs sync:
|
|
60
82
|
|
|
61
83
|
```ts
|
|
62
84
|
import { isReconcileLeadClient } from "@agent-native/core/client";
|
|
63
85
|
|
|
64
86
|
if (
|
|
65
87
|
loaded.updatedAt > lastAppliedUpdatedAt.current &&
|
|
66
|
-
isReconcileLeadClient(
|
|
88
|
+
isReconcileLeadClient(awareness, ydoc.clientID)
|
|
67
89
|
) {
|
|
68
90
|
applyAuthoritativeContent(loaded.content);
|
|
69
91
|
}
|
|
70
92
|
```
|
|
71
93
|
|
|
72
|
-
|
|
94
|
+
The agent's awareness entry (`AGENT_CLIENT_ID`, max int) can never be the
|
|
95
|
+
lead. A sole client is always the lead. The election is deterministic with no
|
|
96
|
+
coordination round-trip.
|
|
73
97
|
|
|
74
98
|
### v1 limitation
|
|
75
99
|
|
|
76
|
-
|
|
100
|
+
Full-content reconcile is **last-writer-wins** for the rare case where a human
|
|
101
|
+
has unsaved edits in the exact region the agent simultaneously rewrites. Edits
|
|
102
|
+
in **different** regions merge fine through the CRDT.
|
|
103
|
+
|
|
104
|
+
## Security
|
|
105
|
+
|
|
106
|
+
### Always set `resourceType`
|
|
107
|
+
|
|
108
|
+
```ts
|
|
109
|
+
// server/plugins/collab.ts
|
|
110
|
+
import { createCollabPlugin } from "@agent-native/core/server";
|
|
111
|
+
|
|
112
|
+
export default createCollabPlugin({
|
|
113
|
+
table: "documents",
|
|
114
|
+
contentColumn: "content",
|
|
115
|
+
idColumn: "id",
|
|
116
|
+
resourceType: "document", // required
|
|
117
|
+
});
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Without `resourceType`, the server logs a one-time warning and collab push
|
|
121
|
+
events are delivered to **all authenticated users** without document-level
|
|
122
|
+
scoping. Set it to the resource type name registered via
|
|
123
|
+
`registerShareableResource`.
|
|
124
|
+
|
|
125
|
+
Non-owner sharees who have explicit access fall back to state-vector catch-up
|
|
126
|
+
(safe, slightly higher latency). Awareness routes require the same viewer
|
|
127
|
+
access as read routes.
|
|
128
|
+
|
|
129
|
+
### Payload limits
|
|
130
|
+
|
|
131
|
+
Write routes reject payloads exceeding `maxPayloadBytes` (default 2 MB) with
|
|
132
|
+
HTTP 413. Override:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
createCollabPlugin({ resourceType: "document", maxPayloadBytes: 512 * 1024 });
|
|
136
|
+
```
|
|
77
137
|
|
|
78
138
|
## Enabling Collaboration
|
|
79
139
|
|
|
80
140
|
### 1. Install packages
|
|
81
141
|
|
|
82
142
|
```bash
|
|
83
|
-
pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap
|
|
143
|
+
pnpm add @tiptap/extension-collaboration @tiptap/extension-collaboration-caret @tiptap/y-tiptap @tiptap/core
|
|
84
144
|
```
|
|
85
145
|
|
|
86
|
-
### 2. Add collab server plugin
|
|
146
|
+
### 2. Add collab server plugin (with `resourceType`)
|
|
87
147
|
|
|
88
148
|
```ts
|
|
89
149
|
// server/plugins/collab.ts
|
|
90
|
-
import { createCollabPlugin } from "@agent-native/core/
|
|
150
|
+
import { createCollabPlugin } from "@agent-native/core/server";
|
|
91
151
|
|
|
92
152
|
export default createCollabPlugin({
|
|
93
153
|
table: "documents",
|
|
94
154
|
contentColumn: "content",
|
|
95
155
|
idColumn: "id",
|
|
156
|
+
resourceType: "document",
|
|
96
157
|
});
|
|
97
158
|
```
|
|
98
159
|
|
|
99
160
|
### 3. Use the client hook
|
|
100
161
|
|
|
101
162
|
```ts
|
|
102
|
-
import { useCollaborativeDoc } from "@agent-native/core/client";
|
|
103
|
-
|
|
104
|
-
const { ydoc,
|
|
163
|
+
import { useCollaborativeDoc, emailToColor, emailToName } from "@agent-native/core/client";
|
|
164
|
+
|
|
165
|
+
const { ydoc, awareness, activeUsers, agentActive, agentPresent } =
|
|
166
|
+
useCollaborativeDoc({
|
|
167
|
+
docId: documentId,
|
|
168
|
+
requestSource: TAB_ID,
|
|
169
|
+
user: {
|
|
170
|
+
name: emailToName(session.email),
|
|
171
|
+
email: session.email,
|
|
172
|
+
color: emailToColor(session.email),
|
|
173
|
+
},
|
|
174
|
+
});
|
|
105
175
|
```
|
|
106
176
|
|
|
107
177
|
### 4. Add TipTap extensions
|
|
@@ -112,12 +182,14 @@ import { CollaborationCaret } from "@tiptap/extension-collaboration-caret";
|
|
|
112
182
|
|
|
113
183
|
const editor = useEditor({
|
|
114
184
|
extensions: [
|
|
185
|
+
StarterKit.configure({ history: false }), // Yjs handles undo
|
|
115
186
|
Collaboration.configure({ document: ydoc }),
|
|
116
187
|
CollaborationCaret.configure({
|
|
117
|
-
provider,
|
|
188
|
+
provider: { awareness },
|
|
118
189
|
user: { name: session.email, color: "#6366f1" },
|
|
119
190
|
}),
|
|
120
191
|
],
|
|
192
|
+
// Do NOT pass content — Yjs owns it
|
|
121
193
|
});
|
|
122
194
|
```
|
|
123
195
|
|
|
@@ -126,6 +198,9 @@ const editor = useEditor({
|
|
|
126
198
|
```ts
|
|
127
199
|
optimizeDeps: {
|
|
128
200
|
include: [
|
|
201
|
+
"yjs",
|
|
202
|
+
"y-protocols/awareness",
|
|
203
|
+
"@tiptap/core",
|
|
129
204
|
"@tiptap/extension-collaboration",
|
|
130
205
|
"@tiptap/extension-collaboration-caret",
|
|
131
206
|
"@tiptap/y-tiptap",
|
|
@@ -137,22 +212,95 @@ optimizeDeps: {
|
|
|
137
212
|
|
|
138
213
|
| Route | Purpose |
|
|
139
214
|
| ----- | ------- |
|
|
140
|
-
| `GET /_agent-native/collab/:docId/state` | Fetch full Y.Doc state |
|
|
215
|
+
| `GET /_agent-native/collab/:docId/state` | Fetch full Y.Doc state (accepts `?stateVector=` for diff) |
|
|
141
216
|
| `POST /_agent-native/collab/:docId/update` | Apply client Yjs update |
|
|
142
217
|
| `POST /_agent-native/collab/:docId/text` | Apply full text (diff-based) |
|
|
143
218
|
| `POST /_agent-native/collab/:docId/search-replace` | Surgical find/replace in Y.XmlFragment |
|
|
219
|
+
| `POST /_agent-native/collab/:docId/json` | Apply full JSON diff to Y.Map/Y.Array |
|
|
220
|
+
| `GET /_agent-native/collab/:docId/json` | Read current JSON state |
|
|
221
|
+
| `POST /_agent-native/collab/:docId/patch` | Surgical JSON patch ops |
|
|
144
222
|
| `POST /_agent-native/collab/:docId/awareness` | Sync cursor/presence state |
|
|
145
223
|
| `GET /_agent-native/collab/:docId/users` | List active users |
|
|
146
224
|
|
|
225
|
+
## Granular Server-Side Merge Pattern
|
|
226
|
+
|
|
227
|
+
For structured documents (slides, forms, design files) where body collab would
|
|
228
|
+
cause LWW conflicts at the container level, use **granular server-side merge**:
|
|
229
|
+
define an action with targeted per-item operations.
|
|
230
|
+
|
|
231
|
+
**When to use granular merge vs body collab:**
|
|
232
|
+
|
|
233
|
+
| Scenario | Recommended approach |
|
|
234
|
+
| -------- | -------------------- |
|
|
235
|
+
| Free-form rich text, cursor-level CRDT matters | Body collab (Y.XmlFragment + TipTap) |
|
|
236
|
+
| Structured items (slides, fields) where different users edit different items | Granular server-side merge (action with patch ops) |
|
|
237
|
+
|
|
238
|
+
Example operation shape for slides:
|
|
239
|
+
|
|
240
|
+
```ts
|
|
241
|
+
type PatchDeckOp =
|
|
242
|
+
| { type: "patch"; slideId: string; fields: Partial<SlideFields> }
|
|
243
|
+
| { type: "add"; position: number; slide: SlideData }
|
|
244
|
+
| { type: "delete"; slideId: string }
|
|
245
|
+
| { type: "reorder"; slideId: string; newIndex: number };
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Concurrent edits to different slides both succeed at the action level; there
|
|
249
|
+
is no whole-deck LWW. Forms use the same shape with field-level ops.
|
|
250
|
+
|
|
251
|
+
## Collaborative Undo Scoping (Y.UndoManager)
|
|
252
|
+
|
|
253
|
+
Scope undo/redo to the local user's own edits so peer and agent changes are
|
|
254
|
+
never accidentally reversed:
|
|
255
|
+
|
|
256
|
+
```ts
|
|
257
|
+
import * as Y from "yjs";
|
|
258
|
+
|
|
259
|
+
const LOCAL_EDIT_ORIGIN = "local";
|
|
260
|
+
|
|
261
|
+
const undoManager = new Y.UndoManager(ydoc.getText("content"), {
|
|
262
|
+
trackedOrigins: new Set([LOCAL_EDIT_ORIGIN]),
|
|
263
|
+
captureTimeout: 800, // coalesces rapid slider drags into one undo step
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
// Mark local edits with the tracked origin
|
|
267
|
+
ydoc.transact(() => {
|
|
268
|
+
// apply local change
|
|
269
|
+
}, LOCAL_EDIT_ORIGIN);
|
|
270
|
+
|
|
271
|
+
undoManager.undo(); // only reverses LOCAL_EDIT_ORIGIN transactions
|
|
272
|
+
undoManager.redo();
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
Rules:
|
|
276
|
+
- Pass a `Set` to `trackedOrigins` — not an array.
|
|
277
|
+
- Remote (`"remote"`) and agent (`"agent"`) origins are never captured.
|
|
278
|
+
- Recreate and destroy the manager when the active document changes.
|
|
279
|
+
|
|
147
280
|
## Common Pitfalls
|
|
148
281
|
|
|
149
|
-
- **
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
- **
|
|
282
|
+
- **Missing `resourceType`** — The server logs a warning on startup and
|
|
283
|
+
delivers collab events to all authenticated users without access scoping.
|
|
284
|
+
Always set `resourceType`.
|
|
285
|
+
- **Don't pass `content` as a TipTap prop** when Collaboration is enabled —
|
|
286
|
+
Yjs owns the content. Seed via `editor.commands.setContent()` only when the
|
|
287
|
+
Y.XmlFragment is empty.
|
|
288
|
+
- **Don't call `editor.setContent()` ad hoc for agent edits** — the only
|
|
289
|
+
sanctioned `setContent` is gated by `updatedAt` and guarded by
|
|
290
|
+
`isReconcileLeadClient`. Calling it from elsewhere duplicates content across
|
|
291
|
+
the CRDT or re-applies stale snapshots.
|
|
292
|
+
- **Add packages to `optimizeDeps`** — Vite won't pre-bundle Yjs correctly
|
|
293
|
+
otherwise, causing runtime errors in dev.
|
|
294
|
+
- **One `Y.Doc` per document** — Don't create multiple Y.Doc instances for the
|
|
295
|
+
same document ID. `useCollaborativeDoc` caches by ID.
|
|
296
|
+
- **Destroy Y.UndoManager on doc change** — Stale managers hold Y.Doc
|
|
297
|
+
references and grow unboundedly. Recreate on `docId` change.
|
|
153
298
|
|
|
154
299
|
## Related Skills
|
|
155
300
|
|
|
156
|
-
- `real-time-sync` — The change-sync system that delivers the `updatedAt` bump
|
|
157
|
-
|
|
158
|
-
- `
|
|
301
|
+
- `real-time-sync` — The change-sync system that delivers the `updatedAt` bump
|
|
302
|
+
driving editor reconciliation; also `useReconciledState` for non-Yjs surfaces
|
|
303
|
+
- `storing-data` — The `_collab_docs` table and SQL canonical content
|
|
304
|
+
- `security` — `registerShareableResource`, `resolveAccess`, `assertAccess`
|
|
305
|
+
- `self-modifying-code` — Agent edits to collaborative documents edit canonical
|
|
306
|
+
SQL content, not raw Yjs
|
|
@@ -49,7 +49,7 @@ The agent modifies data in SQL, but the UI runs in the browser. SSE bridges same
|
|
|
49
49
|
|
|
50
50
|
For list/sidebar queries, use the same pattern — pass the counter into the queryKey of every list query you want to keep fresh.
|
|
51
51
|
|
|
52
|
-
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It runs every 2 seconds until SSE is connected, then relaxes to 15 seconds. If SSE is disabled or unavailable, polling continues at the
|
|
52
|
+
4. **Fallback** polling calls `/_agent-native/poll?since=N`. It runs every 2 seconds until SSE is connected, then relaxes to 15 seconds (`SSE_FALLBACK_INTERVAL_MS`). If SSE is disabled or unavailable (e.g., edge/serverless deployments), polling continues at the 2 s cadence. Polling is the universal serverless fallback — it detects DB timestamp changes even when the write happened in a different process or invocation.
|
|
53
53
|
|
|
54
54
|
5. When the agent writes to the database, the version increments, SSE/polling detects it, and React Query refetches the affected queries.
|
|
55
55
|
|
|
@@ -196,6 +196,16 @@ const [title, setTitle] = useReconciledState(props.title, { active: isEditing })
|
|
|
196
196
|
| Local edit state copied from a server value (inputs, popovers, inline editors) | `useReconciledState(externalValue, { active })` |
|
|
197
197
|
| Collaborative rich-text editor (Yjs) | `updatedAt`-gated reconcile + `isReconcileLeadClient` — see `real-time-collab` |
|
|
198
198
|
|
|
199
|
+
## Granular server-side merge for non-body fields
|
|
200
|
+
|
|
201
|
+
For structured documents (slide decks, form builders, design files) where the
|
|
202
|
+
Yjs body collab would cause LWW conflicts at the container level, pair the
|
|
203
|
+
change-sync `updatedAt` bump with a **granular server-side merge action** that
|
|
204
|
+
accepts targeted per-item operations (add/patch/delete/reorder). Concurrent
|
|
205
|
+
edits to different items both survive at the action level; the `collab` source
|
|
206
|
+
version bump then propagates the merged state to all open clients. See
|
|
207
|
+
`real-time-collab` for the pattern and examples.
|
|
208
|
+
|
|
199
209
|
## Related Skills
|
|
200
210
|
|
|
201
211
|
- **storing-data** — Application-state and settings are data stores that sync through change events
|
|
@@ -203,4 +213,4 @@ const [title, setTitle] = useReconciledState(props.title, { active: isEditing })
|
|
|
203
213
|
- **actions** — Mutating actions trigger change events
|
|
204
214
|
- **client-methods** — Route details belong in helpers/hooks, not components
|
|
205
215
|
- **self-modifying-code** — Agent code edits trigger change events; rapid edits can cause event storms
|
|
206
|
-
- **real-time-collab** — Collaborative editors reconcile agent edits into a shared Y.Doc, driven by the same change-sync `updatedAt` bump
|
|
216
|
+
- **real-time-collab** — Collaborative editors reconcile agent edits into a shared Y.Doc, driven by the same change-sync `updatedAt` bump; also the granular server-side merge pattern for structured data
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { writeWorkspaceFile, appendWorkspaceFile, readWorkspaceFile, getWorkspaceFileMeta, listWorkspaceFiles, deleteWorkspaceFile, grepWorkspaceFiles, validatePath, MAX_FILE_BYTES, MAX_SCOPE_BYTES, SAVE_TO_FILE_MAX_BYTES, type WorkspaceFilesScope, type WorkspaceFile, type WorkspaceFileMeta, } from "./store.js";
|
|
2
|
+
export { createWorkspaceFilesTool } from "./tool.js";
|
|
3
|
+
export { WORKSPACE_FILES_CREATE_SQL, WORKSPACE_FILES_INDEX_SQL, } from "./schema.js";
|
|
4
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/workspace-files/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,sBAAsB,EACtB,KAAK,mBAAmB,EACxB,KAAK,aAAa,EAClB,KAAK,iBAAiB,GACvB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { writeWorkspaceFile, appendWorkspaceFile, readWorkspaceFile, getWorkspaceFileMeta, listWorkspaceFiles, deleteWorkspaceFile, grepWorkspaceFiles, validatePath, MAX_FILE_BYTES, MAX_SCOPE_BYTES, SAVE_TO_FILE_MAX_BYTES, } from "./store.js";
|
|
2
|
+
export { createWorkspaceFilesTool } from "./tool.js";
|
|
3
|
+
export { WORKSPACE_FILES_CREATE_SQL, WORKSPACE_FILES_INDEX_SQL, } from "./schema.js";
|
|
4
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/workspace-files/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,kBAAkB,EAClB,mBAAmB,EACnB,iBAAiB,EACjB,oBAAoB,EACpB,kBAAkB,EAClB,mBAAmB,EACnB,kBAAkB,EAClB,YAAY,EACZ,cAAc,EACd,eAAe,EACf,sBAAsB,GAIvB,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,wBAAwB,EAAE,MAAM,WAAW,CAAC;AACrD,OAAO,EACL,0BAA0B,EAC1B,yBAAyB,GAC1B,MAAM,aAAa,CAAC","sourcesContent":["export {\n writeWorkspaceFile,\n appendWorkspaceFile,\n readWorkspaceFile,\n getWorkspaceFileMeta,\n listWorkspaceFiles,\n deleteWorkspaceFile,\n grepWorkspaceFiles,\n validatePath,\n MAX_FILE_BYTES,\n MAX_SCOPE_BYTES,\n SAVE_TO_FILE_MAX_BYTES,\n type WorkspaceFilesScope,\n type WorkspaceFile,\n type WorkspaceFileMeta,\n} from \"./store.js\";\n\nexport { createWorkspaceFilesTool } from \"./tool.js\";\nexport {\n WORKSPACE_FILES_CREATE_SQL,\n WORKSPACE_FILES_INDEX_SQL,\n} from \"./schema.js\";\n"]}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL schema for workspace_files — durable scratch storage for the agent.
|
|
3
|
+
*
|
|
4
|
+
* Files are scoped to either a user (scope="user", scope_id=email) or a
|
|
5
|
+
* workspace / org (scope="org", scope_id=orgId), mirroring the secrets table
|
|
6
|
+
* pattern. Paths are unique per scope+scope_id pair and may include path
|
|
7
|
+
* separators (e.g. "analysis/2026-q2/step1.md").
|
|
8
|
+
*
|
|
9
|
+
* Size limits:
|
|
10
|
+
* - Per-file content: 2 MB (enforced in the store layer).
|
|
11
|
+
* - Per-scope total: 200 MB (enforced in the store layer).
|
|
12
|
+
*/
|
|
13
|
+
export declare const workspaceFiles: import("drizzle-orm/sqlite-core").SQLiteTableWithColumns<{
|
|
14
|
+
name: "workspace_files";
|
|
15
|
+
schema: undefined;
|
|
16
|
+
columns: {
|
|
17
|
+
id: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
18
|
+
name: "id";
|
|
19
|
+
tableName: "workspace_files";
|
|
20
|
+
dataType: "string";
|
|
21
|
+
columnType: "SQLiteText";
|
|
22
|
+
data: string;
|
|
23
|
+
driverParam: string;
|
|
24
|
+
notNull: true;
|
|
25
|
+
hasDefault: false;
|
|
26
|
+
isPrimaryKey: true;
|
|
27
|
+
isAutoincrement: false;
|
|
28
|
+
hasRuntimeDefault: false;
|
|
29
|
+
enumValues: [string, ...string[]];
|
|
30
|
+
baseColumn: never;
|
|
31
|
+
identity: undefined;
|
|
32
|
+
generated: undefined;
|
|
33
|
+
}, {}, {
|
|
34
|
+
length: number;
|
|
35
|
+
}>;
|
|
36
|
+
scope: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
37
|
+
name: "scope";
|
|
38
|
+
tableName: "workspace_files";
|
|
39
|
+
dataType: "string";
|
|
40
|
+
columnType: "SQLiteText";
|
|
41
|
+
data: string;
|
|
42
|
+
driverParam: string;
|
|
43
|
+
notNull: true;
|
|
44
|
+
hasDefault: false;
|
|
45
|
+
isPrimaryKey: false;
|
|
46
|
+
isAutoincrement: false;
|
|
47
|
+
hasRuntimeDefault: false;
|
|
48
|
+
enumValues: [string, ...string[]];
|
|
49
|
+
baseColumn: never;
|
|
50
|
+
identity: undefined;
|
|
51
|
+
generated: undefined;
|
|
52
|
+
}, {}, {
|
|
53
|
+
length: number;
|
|
54
|
+
}>;
|
|
55
|
+
scopeId: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
56
|
+
name: "scope_id";
|
|
57
|
+
tableName: "workspace_files";
|
|
58
|
+
dataType: "string";
|
|
59
|
+
columnType: "SQLiteText";
|
|
60
|
+
data: string;
|
|
61
|
+
driverParam: string;
|
|
62
|
+
notNull: true;
|
|
63
|
+
hasDefault: false;
|
|
64
|
+
isPrimaryKey: false;
|
|
65
|
+
isAutoincrement: false;
|
|
66
|
+
hasRuntimeDefault: false;
|
|
67
|
+
enumValues: [string, ...string[]];
|
|
68
|
+
baseColumn: never;
|
|
69
|
+
identity: undefined;
|
|
70
|
+
generated: undefined;
|
|
71
|
+
}, {}, {
|
|
72
|
+
length: number;
|
|
73
|
+
}>;
|
|
74
|
+
path: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
75
|
+
name: "path";
|
|
76
|
+
tableName: "workspace_files";
|
|
77
|
+
dataType: "string";
|
|
78
|
+
columnType: "SQLiteText";
|
|
79
|
+
data: string;
|
|
80
|
+
driverParam: string;
|
|
81
|
+
notNull: true;
|
|
82
|
+
hasDefault: false;
|
|
83
|
+
isPrimaryKey: false;
|
|
84
|
+
isAutoincrement: false;
|
|
85
|
+
hasRuntimeDefault: false;
|
|
86
|
+
enumValues: [string, ...string[]];
|
|
87
|
+
baseColumn: never;
|
|
88
|
+
identity: undefined;
|
|
89
|
+
generated: undefined;
|
|
90
|
+
}, {}, {
|
|
91
|
+
length: number;
|
|
92
|
+
}>;
|
|
93
|
+
content: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
94
|
+
name: "content";
|
|
95
|
+
tableName: "workspace_files";
|
|
96
|
+
dataType: "string";
|
|
97
|
+
columnType: "SQLiteText";
|
|
98
|
+
data: string;
|
|
99
|
+
driverParam: string;
|
|
100
|
+
notNull: true;
|
|
101
|
+
hasDefault: true;
|
|
102
|
+
isPrimaryKey: false;
|
|
103
|
+
isAutoincrement: false;
|
|
104
|
+
hasRuntimeDefault: false;
|
|
105
|
+
enumValues: [string, ...string[]];
|
|
106
|
+
baseColumn: never;
|
|
107
|
+
identity: undefined;
|
|
108
|
+
generated: undefined;
|
|
109
|
+
}, {}, {
|
|
110
|
+
length: number;
|
|
111
|
+
}>;
|
|
112
|
+
contentType: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
113
|
+
name: "content_type";
|
|
114
|
+
tableName: "workspace_files";
|
|
115
|
+
dataType: "string";
|
|
116
|
+
columnType: "SQLiteText";
|
|
117
|
+
data: string;
|
|
118
|
+
driverParam: string;
|
|
119
|
+
notNull: true;
|
|
120
|
+
hasDefault: true;
|
|
121
|
+
isPrimaryKey: false;
|
|
122
|
+
isAutoincrement: false;
|
|
123
|
+
hasRuntimeDefault: false;
|
|
124
|
+
enumValues: [string, ...string[]];
|
|
125
|
+
baseColumn: never;
|
|
126
|
+
identity: undefined;
|
|
127
|
+
generated: undefined;
|
|
128
|
+
}, {}, {
|
|
129
|
+
length: number;
|
|
130
|
+
}>;
|
|
131
|
+
sizeBytes: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
132
|
+
name: "size_bytes";
|
|
133
|
+
tableName: "workspace_files";
|
|
134
|
+
dataType: "number";
|
|
135
|
+
columnType: "SQLiteInteger";
|
|
136
|
+
data: number;
|
|
137
|
+
driverParam: number;
|
|
138
|
+
notNull: true;
|
|
139
|
+
hasDefault: true;
|
|
140
|
+
isPrimaryKey: false;
|
|
141
|
+
isAutoincrement: false;
|
|
142
|
+
hasRuntimeDefault: false;
|
|
143
|
+
enumValues: undefined;
|
|
144
|
+
baseColumn: never;
|
|
145
|
+
identity: undefined;
|
|
146
|
+
generated: undefined;
|
|
147
|
+
}, {}, {}>;
|
|
148
|
+
createdAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
149
|
+
name: "created_at";
|
|
150
|
+
tableName: "workspace_files";
|
|
151
|
+
dataType: "string";
|
|
152
|
+
columnType: "SQLiteText";
|
|
153
|
+
data: string;
|
|
154
|
+
driverParam: string;
|
|
155
|
+
notNull: true;
|
|
156
|
+
hasDefault: false;
|
|
157
|
+
isPrimaryKey: false;
|
|
158
|
+
isAutoincrement: false;
|
|
159
|
+
hasRuntimeDefault: false;
|
|
160
|
+
enumValues: [string, ...string[]];
|
|
161
|
+
baseColumn: never;
|
|
162
|
+
identity: undefined;
|
|
163
|
+
generated: undefined;
|
|
164
|
+
}, {}, {
|
|
165
|
+
length: number;
|
|
166
|
+
}>;
|
|
167
|
+
updatedAt: import("drizzle-orm/sqlite-core").SQLiteColumn<{
|
|
168
|
+
name: "updated_at";
|
|
169
|
+
tableName: "workspace_files";
|
|
170
|
+
dataType: "string";
|
|
171
|
+
columnType: "SQLiteText";
|
|
172
|
+
data: string;
|
|
173
|
+
driverParam: string;
|
|
174
|
+
notNull: true;
|
|
175
|
+
hasDefault: false;
|
|
176
|
+
isPrimaryKey: false;
|
|
177
|
+
isAutoincrement: false;
|
|
178
|
+
hasRuntimeDefault: false;
|
|
179
|
+
enumValues: [string, ...string[]];
|
|
180
|
+
baseColumn: never;
|
|
181
|
+
identity: undefined;
|
|
182
|
+
generated: undefined;
|
|
183
|
+
}, {}, {
|
|
184
|
+
length: number;
|
|
185
|
+
}>;
|
|
186
|
+
};
|
|
187
|
+
dialect: "sqlite";
|
|
188
|
+
}>;
|
|
189
|
+
/**
|
|
190
|
+
* Raw CREATE TABLE SQL used by the on-demand migration path.
|
|
191
|
+
* Written for SQLite; the migration runner adapts it for Postgres.
|
|
192
|
+
*/
|
|
193
|
+
export declare const WORKSPACE_FILES_CREATE_SQL = "CREATE TABLE IF NOT EXISTS workspace_files (\n id TEXT PRIMARY KEY,\n scope TEXT NOT NULL,\n scope_id TEXT NOT NULL,\n path TEXT NOT NULL,\n content TEXT NOT NULL DEFAULT '',\n content_type TEXT NOT NULL DEFAULT 'text/plain',\n size_bytes INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(scope, scope_id, path)\n)";
|
|
194
|
+
export declare const WORKSPACE_FILES_INDEX_SQL = "CREATE INDEX IF NOT EXISTS workspace_files_scope_idx ON workspace_files (scope, scope_id, updated_at)";
|
|
195
|
+
//# sourceMappingURL=schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../../src/workspace-files/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAIH,eAAO,MAAM,cAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAgBzB,CAAC;AAEH;;;GAGG;AACH,eAAO,MAAM,0BAA0B,oXAWrC,CAAC;AAEH,eAAO,MAAM,yBAAyB,0GAA0G,CAAC"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SQL schema for workspace_files — durable scratch storage for the agent.
|
|
3
|
+
*
|
|
4
|
+
* Files are scoped to either a user (scope="user", scope_id=email) or a
|
|
5
|
+
* workspace / org (scope="org", scope_id=orgId), mirroring the secrets table
|
|
6
|
+
* pattern. Paths are unique per scope+scope_id pair and may include path
|
|
7
|
+
* separators (e.g. "analysis/2026-q2/step1.md").
|
|
8
|
+
*
|
|
9
|
+
* Size limits:
|
|
10
|
+
* - Per-file content: 2 MB (enforced in the store layer).
|
|
11
|
+
* - Per-scope total: 200 MB (enforced in the store layer).
|
|
12
|
+
*/
|
|
13
|
+
import { table, text, integer } from "../db/schema.js";
|
|
14
|
+
export const workspaceFiles = table("workspace_files", {
|
|
15
|
+
id: text("id").primaryKey(),
|
|
16
|
+
/** "user" or "org" */
|
|
17
|
+
scope: text("scope").notNull(),
|
|
18
|
+
/** Email for user-scope; orgId for org-scope. */
|
|
19
|
+
scopeId: text("scope_id").notNull(),
|
|
20
|
+
/** Relative path within the scope, e.g. "memos/q2.md". Unique per scope+scopeId. */
|
|
21
|
+
path: text("path").notNull(),
|
|
22
|
+
/** File content (text). */
|
|
23
|
+
content: text("content").notNull().default(""),
|
|
24
|
+
/** MIME type, e.g. "text/plain", "application/json". */
|
|
25
|
+
contentType: text("content_type").notNull().default("text/plain"),
|
|
26
|
+
/** Byte length of content (utf-8). */
|
|
27
|
+
sizeBytes: integer("size_bytes").notNull().default(0),
|
|
28
|
+
createdAt: text("created_at").notNull(),
|
|
29
|
+
updatedAt: text("updated_at").notNull(),
|
|
30
|
+
});
|
|
31
|
+
/**
|
|
32
|
+
* Raw CREATE TABLE SQL used by the on-demand migration path.
|
|
33
|
+
* Written for SQLite; the migration runner adapts it for Postgres.
|
|
34
|
+
*/
|
|
35
|
+
export const WORKSPACE_FILES_CREATE_SQL = `CREATE TABLE IF NOT EXISTS workspace_files (
|
|
36
|
+
id TEXT PRIMARY KEY,
|
|
37
|
+
scope TEXT NOT NULL,
|
|
38
|
+
scope_id TEXT NOT NULL,
|
|
39
|
+
path TEXT NOT NULL,
|
|
40
|
+
content TEXT NOT NULL DEFAULT '',
|
|
41
|
+
content_type TEXT NOT NULL DEFAULT 'text/plain',
|
|
42
|
+
size_bytes INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
created_at TEXT NOT NULL,
|
|
44
|
+
updated_at TEXT NOT NULL,
|
|
45
|
+
UNIQUE(scope, scope_id, path)
|
|
46
|
+
)`;
|
|
47
|
+
export const WORKSPACE_FILES_INDEX_SQL = `CREATE INDEX IF NOT EXISTS workspace_files_scope_idx ON workspace_files (scope, scope_id, updated_at)`;
|
|
48
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/workspace-files/schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,iBAAiB,CAAC;AAEvD,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,CAAC,iBAAiB,EAAE;IACrD,EAAE,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC,UAAU,EAAE;IAC3B,sBAAsB;IACtB,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,OAAO,EAAE;IAC9B,iDAAiD;IACjD,OAAO,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC,OAAO,EAAE;IACnC,oFAAoF;IACpF,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,OAAO,EAAE;IAC5B,2BAA2B;IAC3B,OAAO,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;IAC9C,wDAAwD;IACxD,WAAW,EAAE,IAAI,CAAC,cAAc,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,YAAY,CAAC;IACjE,sCAAsC;IACtC,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE,CAAC,OAAO,CAAC,CAAC,CAAC;IACrD,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE;IACvC,SAAS,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC,OAAO,EAAE;CACxC,CAAC,CAAC;AAEH;;;GAGG;AACH,MAAM,CAAC,MAAM,0BAA0B,GAAG;;;;;;;;;;;EAWxC,CAAC;AAEH,MAAM,CAAC,MAAM,yBAAyB,GAAG,uGAAuG,CAAC","sourcesContent":["/**\n * SQL schema for workspace_files — durable scratch storage for the agent.\n *\n * Files are scoped to either a user (scope=\"user\", scope_id=email) or a\n * workspace / org (scope=\"org\", scope_id=orgId), mirroring the secrets table\n * pattern. Paths are unique per scope+scope_id pair and may include path\n * separators (e.g. \"analysis/2026-q2/step1.md\").\n *\n * Size limits:\n * - Per-file content: 2 MB (enforced in the store layer).\n * - Per-scope total: 200 MB (enforced in the store layer).\n */\n\nimport { table, text, integer } from \"../db/schema.js\";\n\nexport const workspaceFiles = table(\"workspace_files\", {\n id: text(\"id\").primaryKey(),\n /** \"user\" or \"org\" */\n scope: text(\"scope\").notNull(),\n /** Email for user-scope; orgId for org-scope. */\n scopeId: text(\"scope_id\").notNull(),\n /** Relative path within the scope, e.g. \"memos/q2.md\". Unique per scope+scopeId. */\n path: text(\"path\").notNull(),\n /** File content (text). */\n content: text(\"content\").notNull().default(\"\"),\n /** MIME type, e.g. \"text/plain\", \"application/json\". */\n contentType: text(\"content_type\").notNull().default(\"text/plain\"),\n /** Byte length of content (utf-8). */\n sizeBytes: integer(\"size_bytes\").notNull().default(0),\n createdAt: text(\"created_at\").notNull(),\n updatedAt: text(\"updated_at\").notNull(),\n});\n\n/**\n * Raw CREATE TABLE SQL used by the on-demand migration path.\n * Written for SQLite; the migration runner adapts it for Postgres.\n */\nexport const WORKSPACE_FILES_CREATE_SQL = `CREATE TABLE IF NOT EXISTS workspace_files (\n id TEXT PRIMARY KEY,\n scope TEXT NOT NULL,\n scope_id TEXT NOT NULL,\n path TEXT NOT NULL,\n content TEXT NOT NULL DEFAULT '',\n content_type TEXT NOT NULL DEFAULT 'text/plain',\n size_bytes INTEGER NOT NULL DEFAULT 0,\n created_at TEXT NOT NULL,\n updated_at TEXT NOT NULL,\n UNIQUE(scope, scope_id, path)\n)`;\n\nexport const WORKSPACE_FILES_INDEX_SQL = `CREATE INDEX IF NOT EXISTS workspace_files_scope_idx ON workspace_files (scope, scope_id, updated_at)`;\n"]}
|