@rool-dev/sdk 0.2.0 → 0.3.0-dev.381ac43

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 CHANGED
@@ -1,12 +1,14 @@
1
1
  # Rool SDK
2
2
 
3
- The TypeScript SDK for Rool Spaces, a persistent and collaborative environment for organizing objects.
3
+ The TypeScript SDK for Rool, a persistent and collaborative environment for organizing objects.
4
4
 
5
- Rool Spaces enables you to build applications where AI operates on a structured world model rather than a text conversation. The context for all AI operations is the full object graph, allowing the system to reason about, update, and expand the state of your application directly.
5
+ Rool enables you to build applications where AI operates on a structured world model rather than a text conversation. The context for all AI operations is the full object graph, allowing the system to reason about, update, and expand the state of your application directly.
6
6
 
7
7
  Use Rool to programmatically instruct agents to generate content, research topics, or reorganize data. The client manages authentication, real-time synchronization, and media storage, supporting both single-user and multi-user workflows.
8
8
 
9
9
  **Core primitives:**
10
+ - **Spaces** — Containers for objects, schema, metadata, and channels
11
+ - **Channels** — Named contexts within a space, each with independent interaction history. All object and AI operations go through a channel.
10
12
  - **Objects** — Key-value records with any fields you define. References between objects are data fields whose values are object IDs.
11
13
  - **AI operations** — Create, update, or query objects using natural language and `{{placeholders}}`
12
14
 
@@ -30,48 +32,79 @@ if (!authenticated) {
30
32
  client.login('My App'); // Redirects to auth page, shows "Sign in to My App"
31
33
  }
32
34
 
33
- // Create a new space (or open existing with client.openSpace('id'))
35
+ // Create a new space, then open a channel on it
34
36
  const space = await client.createSpace('Solar System');
37
+ const channel = await space.openChannel('main');
38
+
39
+ // Define the schema — what types of objects exist and their fields
40
+ await channel.createCollection('body', [
41
+ { name: 'name', type: { kind: 'string' } },
42
+ { name: 'mass', type: { kind: 'string' } },
43
+ { name: 'radius', type: { kind: 'string' } },
44
+ { name: 'orbits', type: { kind: 'maybe', inner: { kind: 'ref' } } },
45
+ ]);
35
46
 
36
47
  // Create objects with AI-generated content using {{placeholders}}
37
- const { object: sun } = await space.createObject({
48
+ const { object: sun } = await channel.createObject({
38
49
  data: {
39
- type: 'star',
40
50
  name: 'Sun',
41
51
  mass: '{{mass in solar masses}}',
42
- temperature: '{{surface temperature in Kelvin}}'
52
+ radius: '{{radius in km}}'
43
53
  }
44
54
  });
45
55
 
46
- const { object: earth } = await space.createObject({
56
+ const { object: earth } = await channel.createObject({
47
57
  data: {
48
- type: 'planet',
49
58
  name: 'Earth',
50
59
  mass: '{{mass in Earth masses}}',
51
60
  radius: '{{radius in km}}',
52
- orbitalPeriod: '{{orbital period in days}}',
53
61
  orbits: sun.id // Reference to the sun object
54
62
  }
55
63
  });
56
64
 
57
65
  // Use the AI agent to work with your data
58
- const { message, objects } = await space.prompt(
66
+ const { message, objects } = await channel.prompt(
59
67
  'Add the other planets in our solar system, each referencing the Sun'
60
68
  );
61
69
  console.log(message); // AI explains what it did
62
70
  console.log(`Created ${objects.length} objects`);
63
71
 
64
72
  // Query with natural language
65
- const { objects: innerPlanets } = await space.findObjects({
73
+ const { objects: innerPlanets } = await channel.findObjects({
66
74
  prompt: 'planets closer to the sun than Earth'
67
75
  });
68
76
 
69
77
  // Clean up
70
- space.close();
78
+ channel.close();
71
79
  ```
72
80
 
73
81
  ## Core Concepts
74
82
 
83
+ ### Spaces and Channels
84
+
85
+ A **space** is a container that holds objects, schema, metadata, and channels. A **channel** is a named context within a space — it's the handle you use for all object and AI operations. Each channel has its own interaction history.
86
+
87
+ There are two main handles:
88
+ - **`RoolSpace`** — Lightweight admin handle for user management, link access, channel management, and export. No real-time subscription.
89
+ - **`RoolChannel`** — Full real-time handle for objects, AI prompts, media, schema, and undo/redo.
90
+
91
+ ```typescript
92
+ // Open a space for admin operations
93
+ const space = await client.openSpace('space-id');
94
+ await space.addUser(userId, 'editor');
95
+ await space.setLinkAccess('viewer');
96
+
97
+ // Open a channel for object and AI operations
98
+ const channel = await client.openChannel('space-id', 'my-channel');
99
+ await channel.prompt('Create some planets');
100
+
101
+ // Or open a channel via the space handle
102
+ const channel2 = await space.openChannel('research');
103
+ await channel2.prompt('Analyze the data'); // Independent channel
104
+ ```
105
+
106
+ The `channelId` is fixed when you open a channel and cannot be changed. To use a different channel, open a new one. Both channels share the same objects and schema — only the interaction history differs.
107
+
75
108
  ### Objects & References
76
109
 
77
110
  **Objects** are plain key-value records. The `id` field is reserved; everything else is application-defined.
@@ -98,7 +131,7 @@ Use `{{description}}` in field values to have AI generate content:
98
131
 
99
132
  ```typescript
100
133
  // Create with AI-generated content
101
- await space.createObject({
134
+ await channel.createObject({
102
135
  data: {
103
136
  type: 'article',
104
137
  headline: '{{catchy headline about coffee}}',
@@ -107,12 +140,12 @@ await space.createObject({
107
140
  });
108
141
 
109
142
  // Update existing content with AI
110
- await space.updateObject('abc123', {
143
+ await channel.updateObject('abc123', {
111
144
  prompt: 'Make the body shorter and more casual'
112
145
  });
113
146
 
114
147
  // Add new AI-generated field to existing object
115
- await space.updateObject('abc123', {
148
+ await channel.updateObject('abc123', {
116
149
  data: { summary: '{{one-sentence summary}}' }
117
150
  });
118
151
  ```
@@ -127,17 +160,17 @@ Undo/redo works on **checkpoints**, not individual operations. Call `checkpoint(
127
160
 
128
161
  ```typescript
129
162
  // Create a checkpoint before user action
130
- await space.checkpoint('Delete object');
131
- await space.deleteObjects([objectId]);
163
+ await channel.checkpoint('Delete object');
164
+ await channel.deleteObjects([objectId]);
132
165
 
133
166
  // User can now undo back to the checkpoint
134
- if (await space.canUndo()) {
135
- await space.undo(); // Restores the deleted object
167
+ if (await channel.canUndo()) {
168
+ await channel.undo(); // Restores the deleted object
136
169
  }
137
170
 
138
171
  // Redo reapplies the undone action
139
- if (await space.canRedo()) {
140
- await space.redo(); // Deletes the object again
172
+ if (await channel.canRedo()) {
173
+ await channel.redo(); // Deletes the object again
141
174
  }
142
175
  ```
143
176
 
@@ -147,10 +180,10 @@ In collaborative scenarios, conflicting changes (modified by others since your c
147
180
 
148
181
  ### Hidden Fields
149
182
 
150
- Fields starting with `_` (e.g., `_ui`, `_cache`) are hidden from AI but otherwise behave like normal fields they sync in real-time, persist to the server, support undo/redo, and are visible to all users of the Space. Use them for UI state, positions, or other data the AI shouldn't see or modify:
183
+ Fields starting with `_` (e.g., `_ui`, `_cache`) are hidden from AI and ignored by the schema — you can add them to any object regardless of its collection definition. Otherwise they behave like normal fields: they sync in real-time, persist to the server, support undo/redo, and are visible to all users of the Space. Use them for UI state, positions, or other data the AI shouldn't see or modify:
151
184
 
152
185
  ```typescript
153
- await space.createObject({
186
+ await channel.createObject({
154
187
  data: {
155
188
  title: 'My Article',
156
189
  author: "John Doe",
@@ -170,7 +203,7 @@ Events fire for both local and remote changes. The `source` field indicates orig
170
203
 
171
204
  ```typescript
172
205
  // All UI updates happen in one place, regardless of change source
173
- space.on('objectUpdated', ({ objectId, object, source }) => {
206
+ channel.on('objectUpdated', ({ objectId, object, source }) => {
174
207
  renderObject(objectId, object);
175
208
  if (source === 'remote_agent') {
176
209
  doLayout(); // AI might have added content
@@ -178,7 +211,7 @@ space.on('objectUpdated', ({ objectId, object, source }) => {
178
211
  });
179
212
 
180
213
  // Caller just makes the change - event handler does the UI work
181
- space.updateObject(objectId, { prompt: 'expand this' });
214
+ channel.updateObject(objectId, { prompt: 'expand this' });
182
215
  ```
183
216
 
184
217
  ### Custom Object IDs
@@ -186,7 +219,7 @@ space.updateObject(objectId, { prompt: 'expand this' });
186
219
  By default, `createObject` generates a 6-character alphanumeric ID. Provide your own via `data.id`:
187
220
 
188
221
  ```typescript
189
- await space.createObject({ data: { id: 'article-42', title: 'The Meaning of Life' } });
222
+ await channel.createObject({ data: { id: 'article-42', title: 'The Meaning of Life' } });
190
223
  ```
191
224
 
192
225
  **Why use custom IDs?**
@@ -196,8 +229,8 @@ await space.createObject({ data: { id: 'article-42', title: 'The Meaning of Life
196
229
  ```typescript
197
230
  // Fire-and-forget: create and reference without waiting
198
231
  const id = RoolClient.generateId();
199
- space.createObject({ data: { id, type: 'note', text: '{{expand this idea}}' } });
200
- space.updateObject(parentId, { data: { notes: [...existingNotes, id] } }); // Add reference immediately
232
+ channel.createObject({ data: { id, type: 'note', text: '{{expand this idea}}' } });
233
+ channel.updateObject(parentId, { data: { notes: [...existingNotes, id] } }); // Add reference immediately
201
234
  ```
202
235
 
203
236
  **Constraints:**
@@ -252,7 +285,7 @@ if (!authenticated) {
252
285
  The `prompt()` method is the primary way to invoke the AI agent. The agent has editor-level capabilities — it can create, modify, and delete objects — but cannot see or modify `_`-prefixed fields.
253
286
 
254
287
  ```typescript
255
- const { message, objects } = await space.prompt(
288
+ const { message, objects } = await channel.prompt(
256
289
  "Create a topic node for the solar system, then child nodes for each planet."
257
290
  );
258
291
  console.log(`AI: ${message}`);
@@ -273,12 +306,12 @@ Returns a message (the AI's response) and the list of objects that were created
273
306
 
274
307
  | Option | Description |
275
308
  |--------|-------------|
276
- | `objectIds` | Limit context to specific objects |
309
+ | `objectIds` | Focus the AI on specific objects (given primary attention in context) |
277
310
  | `responseSchema` | Request structured JSON instead of text summary |
278
311
  | `effort` | Effort level: `'QUICK'`, `'STANDARD'` (default), `'REASONING'`, or `'RESEARCH'` |
279
- | `ephemeral` | If true, don't record in conversation history (useful for tab completion) |
312
+ | `ephemeral` | If true, don't record in interaction history (useful for tab completion) |
280
313
  | `readOnly` | If true, disable mutation tools (create, update, delete). Use for questions. |
281
- | `attachments` | Files to attach (`File`, `Blob`, or `{ data, contentType }`). Uploaded to the media store via `uploadMedia()`. Resulting URLs are stored on the interaction's `attachments` field for UI rendering. **Currently only images are interpreted by the AI**; other file types are uploaded and stored but the AI cannot read their contents. |
314
+ | `attachments` | Files to attach (`File`, `Blob`, or `{ data, contentType }`). Uploaded to the media store via `uploadMedia()`. Resulting URLs are stored on the interaction's `attachments` field for UI rendering. The AI can interpret images (JPEG, PNG, GIF, WebP, SVG), PDFs, text-based files (plain text, Markdown, CSV, HTML, XML, JSON), and DOCX documents. Other file types are uploaded and stored but the AI cannot read their contents. |
282
315
 
283
316
  ### Effort Levels
284
317
 
@@ -287,37 +320,37 @@ Returns a message (the AI's response) and the list of objects that were created
287
320
  | `QUICK` | Fast, lightweight model. Best for simple questions. |
288
321
  | `STANDARD` | Default behavior with balanced capabilities. |
289
322
  | `REASONING` | Extended reasoning for complex tasks. |
290
- | `RESEARCH` | Pre-analysis and context gathering (reserved for future use). |
323
+ | `RESEARCH` | Most thorough mode with deep analysis. Slowest and most credit-intensive. |
291
324
 
292
325
  ### Examples
293
326
 
294
327
  ```typescript
295
328
  // Reorganize existing objects
296
- const { objects } = await space.prompt(
329
+ const { objects } = await channel.prompt(
297
330
  "Group these notes by topic and create a parent node for each group."
298
331
  );
299
332
 
300
333
  // Work with specific objects
301
- const result = await space.prompt(
334
+ const result = await channel.prompt(
302
335
  "Summarize these articles",
303
336
  { objectIds: ['article-1', 'article-2'] }
304
337
  );
305
338
 
306
339
  // Quick question without mutations (fast model + read-only)
307
- const { message } = await space.prompt(
340
+ const { message } = await channel.prompt(
308
341
  "What topics are covered?",
309
342
  { effort: 'QUICK', readOnly: true }
310
343
  );
311
344
 
312
345
  // Complex analysis with extended reasoning
313
- await space.prompt(
346
+ await channel.prompt(
314
347
  "Analyze relationships and reorganize",
315
348
  { effort: 'REASONING' }
316
349
  );
317
350
 
318
351
  // Attach files for the AI to see (File from <input>, Blob, or base64)
319
352
  const file = fileInput.files[0]; // from <input type="file">
320
- await space.prompt(
353
+ await channel.prompt(
321
354
  "Describe what's in this photo and create an object for it",
322
355
  { attachments: [file] }
323
356
  );
@@ -328,7 +361,7 @@ await space.prompt(
328
361
  Use `responseSchema` to get structured JSON instead of a text message:
329
362
 
330
363
  ```typescript
331
- const { message } = await space.prompt("Categorize these items", {
364
+ const { message } = await channel.prompt("Categorize these items", {
332
365
  objectIds: ['item-1', 'item-2', 'item-3'],
333
366
  responseSchema: {
334
367
  type: 'object',
@@ -349,7 +382,7 @@ console.log(result.categories, result.summary);
349
382
  ### Context Flow
350
383
 
351
384
  AI operations automatically receive context:
352
- - **Interaction history** — Previous interactions and their results from this conversation
385
+ - **Interaction history** — Previous interactions and their results from this channel
353
386
  - **Recently modified objects** — Objects created or changed recently
354
387
  - **Selected objects** — Objects passed via `objectIds` are given primary focus
355
388
 
@@ -369,6 +402,7 @@ if (!user) {
369
402
  }
370
403
 
371
404
  // Add them to the space
405
+ const space = await client.openSpace('space-id');
372
406
  await space.addUser(user.id, 'editor');
373
407
  ```
374
408
 
@@ -383,6 +417,8 @@ await space.addUser(user.id, 'editor');
383
417
 
384
418
  ### Space Collaboration Methods
385
419
 
420
+ These methods are available on `RoolSpace`:
421
+
386
422
  | Method | Description |
387
423
  |--------|-------------|
388
424
  | `listUsers(): Promise<SpaceMember[]>` | List users with access |
@@ -395,6 +431,8 @@ await space.addUser(user.id, 'editor');
395
431
  Enable public URL access to allow anyone with the space URL to access it:
396
432
 
397
433
  ```typescript
434
+ const space = await client.openSpace('space-id');
435
+
398
436
  // Allow anyone with the URL to view
399
437
  await space.setLinkAccess('viewer');
400
438
 
@@ -423,7 +461,7 @@ When a user accesses a space via URL, they're granted the corresponding role (`v
423
461
  When multiple users have a space open, changes sync in real-time. The `source` field in events tells you who made the change:
424
462
 
425
463
  ```typescript
426
- space.on('objectUpdated', ({ objectId, object, source }) => {
464
+ channel.on('objectUpdated', ({ objectId, object, source }) => {
427
465
  if (source === 'remote_user') {
428
466
  // Another user made this change
429
467
  showCollaboratorActivity(object);
@@ -452,14 +490,28 @@ const client = new RoolClient({
452
490
  });
453
491
  ```
454
492
 
455
- ### Space Lifecycle
493
+ ### Space & Channel Lifecycle
456
494
 
457
495
  | Method | Description |
458
496
  |--------|-------------|
459
497
  | `listSpaces(): Promise<RoolSpaceInfo[]>` | List available spaces |
460
- | `openSpace(id, options?): Promise<RoolSpace>` | Open a space for editing. Options: `{ conversationId?: string }` |
461
- | `createSpace(name?, options?): Promise<RoolSpace>` | Create a new space. Options: `{ conversationId?: string }` |
498
+ | `openSpace(spaceId): Promise<RoolSpace>` | Open a space for admin operations (no real-time subscription) |
499
+ | `openChannel(spaceId, channelId): Promise<RoolChannel>` | Open a channel on a space |
500
+ | `createSpace(name): Promise<RoolSpace>` | Create a new space, returns admin handle |
462
501
  | `deleteSpace(id): Promise<void>` | Permanently delete a space (cannot be undone) |
502
+ | `importArchive(name, archive): Promise<RoolSpace>` | Import from a zip archive, creating a new space |
503
+
504
+ ### Channel Management
505
+
506
+ Manage channels within a space. Available on both the client and space handles:
507
+
508
+ | Method | Description |
509
+ |--------|-------------|
510
+ | `client.renameChannel(spaceId, channelId, name): Promise<void>` | Rename a channel |
511
+ | `client.deleteChannel(spaceId, channelId): Promise<void>` | Delete a channel and its interaction history |
512
+ | `space.getChannels(): ChannelInfo[]` | List channels (from cached snapshot) |
513
+ | `space.deleteChannel(channelId): Promise<void>` | Delete a channel |
514
+ | `channel.rename(name): Promise<void>` | Rename the current channel |
463
515
 
464
516
  ### User Storage
465
517
 
@@ -516,6 +568,9 @@ client.on('authStateChanged', (authenticated: boolean) => void)
516
568
  client.on('spaceAdded', (space: RoolSpaceInfo) => void) // Space created or access granted
517
569
  client.on('spaceRemoved', (spaceId: string) => void) // Space deleted or access revoked
518
570
  client.on('spaceRenamed', (spaceId: string, newName: string) => void)
571
+ client.on('channelCreated', (spaceId: string, channel: ChannelInfo) => void)
572
+ client.on('channelRenamed', (spaceId: string, channelId: string, newName: string) => void)
573
+ client.on('channelDeleted', (spaceId: string, channelId: string) => void)
519
574
  client.on('userStorageChanged', ({ key, value, source }: UserStorageChangedEvent) => void)
520
575
  client.on('connectionStateChanged', (state: 'connected' | 'disconnected' | 'reconnecting') => void)
521
576
  client.on('error', (error: Error, context?: string) => void)
@@ -535,7 +590,36 @@ client.on('spaceRenamed', (id, name) => {
535
590
 
536
591
  ## RoolSpace API
537
592
 
538
- Spaces are first-class objects with built-in undo/redo, event emission, and real-time sync.
593
+ A space is a lightweight admin handle for space-level operations. It does not have a real-time subscription — use channels for live data and object operations.
594
+
595
+ ### Properties
596
+
597
+ | Property | Description |
598
+ |----------|-------------|
599
+ | `id: string` | Space ID |
600
+ | `name: string` | Space name |
601
+ | `role: RoolUserRole` | User's role |
602
+ | `linkAccess: LinkAccess` | URL sharing level |
603
+
604
+ ### Methods
605
+
606
+ | Method | Description |
607
+ |--------|-------------|
608
+ | `openChannel(channelId): Promise<RoolChannel>` | Open a channel on this space |
609
+ | `rename(newName): Promise<void>` | Rename this space |
610
+ | `delete(): Promise<void>` | Permanently delete this space |
611
+ | `listUsers(): Promise<SpaceMember[]>` | List users with access |
612
+ | `addUser(userId, role): Promise<void>` | Add user to space |
613
+ | `removeUser(userId): Promise<void>` | Remove user from space |
614
+ | `setLinkAccess(linkAccess): Promise<void>` | Set URL sharing level |
615
+ | `getChannels(): ChannelInfo[]` | List channels (from cached snapshot) |
616
+ | `deleteChannel(channelId): Promise<void>` | Delete a channel |
617
+ | `exportArchive(): Promise<Blob>` | Export space as zip archive |
618
+ | `refresh(): Promise<void>` | Refresh space data from server |
619
+
620
+ ## RoolChannel API
621
+
622
+ A channel is a named context within a space. All object operations, AI prompts, and real-time sync go through a channel. The `channelId` is fixed at open time — to use a different channel, open a new one.
539
623
 
540
624
  ### Properties
541
625
 
@@ -546,24 +630,24 @@ Spaces are first-class objects with built-in undo/redo, event emission, and real
546
630
  | `role: RoolUserRole` | User's role (`'owner' \| 'admin' \| 'editor' \| 'viewer'`) |
547
631
  | `linkAccess: LinkAccess` | URL sharing level (`'none' \| 'viewer' \| 'editor'`) |
548
632
  | `userId: string` | Current user's ID |
549
- | `conversationId: string` | ID for interaction history (tracks AI context). Writable — set to switch conversations. |
550
- | `isReadOnly(): boolean` | True if viewer role |
633
+ | `channelId: string` | Channel ID (read-only, fixed at open time) |
634
+ | `isReadOnly: boolean` | True if viewer role |
551
635
 
552
636
  ### Lifecycle
553
637
 
554
638
  | Method | Description |
555
639
  |--------|-------------|
556
640
  | `close(): void` | Clean up resources and stop receiving updates |
557
- | `rename(newName): Promise<void>` | Rename the space |
641
+ | `rename(name): Promise<void>` | Rename this channel |
558
642
 
559
643
  ### Object Operations
560
644
 
561
- Objects are plain key/value records. `id` is the only reserved field; everything else is application-defined. References between objects are data fields whose values are object IDs.
645
+ Objects are plain key/value records. `id` is the only reserved field; everything else is application-defined. References between objects are data fields whose values are object IDs. All objects must belong to a collection (see below in the schema section). Before adding a new type of object, update the schema in the space.
562
646
 
563
647
  | Method | Description |
564
648
  |--------|-------------|
565
649
  | `getObject(objectId): Promise<RoolObject \| undefined>` | Get object data, or undefined if not found. |
566
- | `stat(objectId): Promise<RoolObjectStat \| undefined>` | Get object stat (audit info: modifiedAt, modifiedBy, modifiedByName), or undefined if not found. |
650
+ | `stat(objectId): RoolObjectStat \| undefined` | Get object stat (audit info: modifiedAt, modifiedBy, modifiedByName), or undefined if not found. Sync read from local cache. |
567
651
  | `findObjects(options): Promise<{ objects, message }>` | Find objects using structured filters and natural language. Results sorted by modifiedAt (desc by default). |
568
652
  | `getObjectIds(options?): string[]` | Get all object IDs. Sorted by modifiedAt (desc by default). Options: `{ limit?, order? }`. |
569
653
  | `createObject(options): Promise<{ object, message }>` | Create a new object. Returns the object (with AI-filled content) and message. |
@@ -575,7 +659,7 @@ Objects are plain key/value records. `id` is the only reserved field; everything
575
659
  | Option | Description |
576
660
  |--------|-------------|
577
661
  | `data` | Object data fields (required). Include `id` to use a custom ID. Use `{{placeholder}}` for AI-generated content. Fields prefixed with `_` are hidden from AI. |
578
- | `ephemeral` | If true, the operation won't be recorded in conversation history. Useful for transient operations. |
662
+ | `ephemeral` | If true, the operation won't be recorded in interaction history. Useful for transient operations. |
579
663
 
580
664
  #### updateObject Options
581
665
 
@@ -583,7 +667,7 @@ Objects are plain key/value records. `id` is the only reserved field; everything
583
667
  |--------|-------------|
584
668
  | `data` | Fields to add or update. Pass `null`/`undefined` to delete a field. Use `{{placeholder}}` for AI-generated content. Fields prefixed with `_` are hidden from AI. |
585
669
  | `prompt` | Natural language instruction for AI to modify content. |
586
- | `ephemeral` | If true, the operation won't be recorded in conversation history. Useful for transient operations. |
670
+ | `ephemeral` | If true, the operation won't be recorded in interaction history. Useful for transient operations. |
587
671
 
588
672
  #### findObjects Options
589
673
 
@@ -600,23 +684,23 @@ Find objects using structured filters and/or natural language.
600
684
  | `limit` | Maximum number of results. |
601
685
  | `objectIds` | Scope to specific object IDs. Constrains the candidate set in both structured and AI queries. |
602
686
  | `order` | Sort order by modifiedAt: `'asc'` or `'desc'` (default: `'desc'`). |
603
- | `ephemeral` | If true, the query won't be recorded in conversation history. Useful for responsive search. |
687
+ | `ephemeral` | If true, the query won't be recorded in interaction history. Useful for responsive search. |
604
688
 
605
689
  **Examples:**
606
690
 
607
691
  ```typescript
608
692
  // Exact field matching (no AI, no credits)
609
- const { objects } = await space.findObjects({
693
+ const { objects } = await channel.findObjects({
610
694
  where: { type: 'article', status: 'published' }
611
695
  });
612
696
 
613
697
  // Pure natural language query (AI interprets)
614
- const { objects, message } = await space.findObjects({
698
+ const { objects, message } = await channel.findObjects({
615
699
  prompt: 'articles about space exploration published this year'
616
700
  });
617
701
 
618
702
  // Combined: where narrows the data, prompt queries within it
619
- const { objects } = await space.findObjects({
703
+ const { objects } = await channel.findObjects({
620
704
  where: { type: 'article' },
621
705
  prompt: 'that discuss climate solutions positively',
622
706
  limit: 10
@@ -655,39 +739,91 @@ Media URLs in object fields are visible to AI. Both uploaded and AI-generated me
655
739
  | Method | Description |
656
740
  |--------|-------------|
657
741
  | `uploadMedia(file): Promise<string>` | Upload file, returns URL |
658
- | `fetchMedia(url): Promise<MediaResponse>` | Fetch any URL, returns headers and blob() method (adds auth for backend URLs, works for external URLs too) |
742
+ | `fetchMedia(url, options?): Promise<MediaResponse>` | Fetch any URL, returns headers and blob() method (adds auth for backend URLs, works for external URLs too). Pass `{ forceProxy: true }` to skip the direct fetch and route through the server proxy immediately. |
659
743
  | `deleteMedia(url): Promise<void>` | Delete media file by URL |
660
744
  | `listMedia(): Promise<MediaInfo[]>` | List all media with metadata |
661
745
 
662
746
  ```typescript
663
747
  // Upload an image
664
- const url = await space.uploadMedia(file);
665
- await space.createObject({ data: { title: 'Photo', image: url } });
748
+ const url = await channel.uploadMedia(file);
749
+ await channel.createObject({ data: { title: 'Photo', image: url } });
666
750
 
667
751
  // Or let AI generate one using a placeholder
668
- await space.createObject({
752
+ await channel.createObject({
669
753
  data: { title: 'Mascot', image: '{{generate an image of a flying tortoise}}' }
670
754
  });
671
755
 
672
756
  // Display media (handles auth automatically)
673
- const response = await space.fetchMedia(object.image);
757
+ const response = await channel.fetchMedia(object.image);
674
758
  if (response.contentType.startsWith('image/')) {
675
759
  const blob = await response.blob();
676
760
  img.src = URL.createObjectURL(blob);
677
761
  }
678
762
  ```
679
763
 
764
+ ### Collection Schema
765
+
766
+ Collections are types you can use to group objects in a space. Every object must belong to a collection. Collections make up the schema and are stored in the space data, syncing in real time together with the rest of the space. The schema is also visible to the AI agent, which it can use to understand what collections exist and what fields they contain, producing more consistent objects.
767
+
768
+
769
+ ```typescript
770
+ // Define a collection with typed fields
771
+ await channel.createCollection('article', [
772
+ { name: 'title', type: { kind: 'string' } },
773
+ { name: 'status', type: { kind: 'enum', values: ['draft', 'published', 'archived'] } },
774
+ { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
775
+ { name: 'author', type: { kind: 'ref' } },
776
+ ]);
777
+
778
+ // Read the current schema
779
+ const schema = channel.getSchema();
780
+ console.log(schema.article.fields); // FieldDef[]
781
+
782
+ // Modify an existing collection's fields
783
+ await channel.alterCollection('article', [
784
+ { name: 'title', type: { kind: 'string' } },
785
+ { name: 'status', type: { kind: 'enum', values: ['draft', 'review', 'published', 'archived'] } },
786
+ { name: 'tags', type: { kind: 'array', inner: { kind: 'string' } } },
787
+ { name: 'author', type: { kind: 'ref' } },
788
+ { name: 'wordCount', type: { kind: 'number' } },
789
+ ]);
790
+
791
+ // Remove a collection
792
+ await channel.dropCollection('article');
793
+ ```
794
+
795
+ | Method | Description |
796
+ |--------|-------------|
797
+ | `getSchema(): SpaceSchema` | Get all collection definitions |
798
+ | `createCollection(name, fields): Promise<CollectionDef>` | Add a new collection to the schema |
799
+ | `alterCollection(name, fields): Promise<CollectionDef>` | Replace a collection's field definitions |
800
+ | `dropCollection(name): Promise<void>` | Remove a collection from the schema |
801
+
802
+ #### Field Types
803
+
804
+ | Kind | Description | Example |
805
+ |------|-------------|---------|
806
+ | `string` | Text value | `{ kind: 'string' }` |
807
+ | `number` | Numeric value | `{ kind: 'number' }` |
808
+ | `boolean` | True/false | `{ kind: 'boolean' }` |
809
+ | `ref` | Reference to another object | `{ kind: 'ref' }` |
810
+ | `enum` | One of a set of values | `{ kind: 'enum', values: ['a', 'b'] }` |
811
+ | `literal` | Exact value | `{ kind: 'literal', value: 'fixed' }` |
812
+ | `array` | List of values | `{ kind: 'array', inner: { kind: 'string' } }` |
813
+ | `maybe` | Optional (nullable) | `{ kind: 'maybe', inner: { kind: 'number' } }` |
814
+
680
815
  ### Import/Export
681
816
 
682
817
  Export and import space data as zip archives for backup, portability, or migration:
683
818
 
684
819
  | Method | Description |
685
820
  |--------|-------------|
686
- | `space.exportArchive(): Promise<Blob>` | Export objects, metadata, conversations, and media as a zip archive |
821
+ | `space.exportArchive(): Promise<Blob>` | Export objects, metadata, channels, and media as a zip archive |
687
822
  | `client.importArchive(name, archive): Promise<RoolSpace>` | Import from a zip archive, creating a new space |
688
823
 
689
824
  **Export:**
690
825
  ```typescript
826
+ const space = await client.openSpace('space-id');
691
827
  const archive = await space.exportArchive();
692
828
  // Save as .zip file
693
829
  const url = URL.createObjectURL(archive);
@@ -695,12 +831,13 @@ const url = URL.createObjectURL(archive);
695
831
 
696
832
  **Import:**
697
833
  ```typescript
698
- const newSpace = await client.importArchive('Imported Data', archiveBlob);
834
+ const space = await client.importArchive('Imported Data', archiveBlob);
835
+ const channel = await space.openChannel('main');
699
836
  ```
700
837
 
701
- The archive format bundles `data.json` (with objects, metadata, and conversations) and a `media/` folder containing all media files. Media URLs are rewritten to relative paths within the archive and restored on import.
838
+ The archive format bundles `data.json` (with objects, metadata, and channels) and a `media/` folder containing all media files. Media URLs are rewritten to relative paths within the archive and restored on import.
702
839
 
703
- ### Space Events
840
+ ### Channel Events
704
841
 
705
842
  Semantic events describe what changed. Events fire for both local changes and remote changes.
706
843
 
@@ -712,27 +849,21 @@ Semantic events describe what changed. Events fire for both local changes and re
712
849
  // - 'system': Resync after error
713
850
 
714
851
  // Object events
715
- space.on('objectCreated', ({ objectId, object, source }) => void)
716
- space.on('objectUpdated', ({ objectId, object, source }) => void)
717
- space.on('objectDeleted', ({ objectId, source }) => void)
852
+ channel.on('objectCreated', ({ objectId, object, source }) => void)
853
+ channel.on('objectUpdated', ({ objectId, object, source }) => void)
854
+ channel.on('objectDeleted', ({ objectId, source }) => void)
718
855
 
719
856
  // Space metadata
720
- space.on('metadataUpdated', ({ metadata, source }) => void)
857
+ channel.on('metadataUpdated', ({ metadata, source }) => void)
721
858
 
722
- // Conversation updated (fetch with getInteractions())
723
- space.on('conversationUpdated', ({ conversationId, source }) => void)
724
-
725
- // Conversation list changed (created, deleted, renamed)
726
- space.on('conversationsChanged', ({ action, conversationId, name, source }) => void)
859
+ // Channel updated (fetch with getInteractions())
860
+ channel.on('channelUpdated', ({ channelId, source }) => void)
727
861
 
728
862
  // Full state replacement (undo/redo, resync after error)
729
- space.on('reset', ({ source }) => void)
730
-
731
- // ConversationId was changed on the space
732
- space.on('conversationIdChanged', ({ previousConversationId, newConversationId }) => void)
863
+ channel.on('reset', ({ source }) => void)
733
864
 
734
- // Sync error occurred, space resynced from server
735
- space.on('syncError', (error: Error) => void)
865
+ // Sync error occurred, channel resynced from server
866
+ channel.on('syncError', (error: Error) => void)
736
867
  ```
737
868
 
738
869
  ### Error Handling
@@ -741,7 +872,7 @@ AI operations may fail due to rate limiting or other transient errors. Check `er
741
872
 
742
873
  ```typescript
743
874
  try {
744
- await space.updateObject(objectId, { prompt: 'expand this' });
875
+ await channel.updateObject(objectId, { prompt: 'expand this' });
745
876
  } catch (error) {
746
877
  if (error.message.includes('temporarily unavailable')) {
747
878
  showToast('Service busy, please try again in a moment');
@@ -751,21 +882,15 @@ try {
751
882
  }
752
883
  ```
753
884
 
754
- ### Internal / Advanced
755
-
756
- | Method | Description |
757
- |--------|-------------|
758
- | `getData(): RoolSpaceData` | Get full space data (internal format) |
759
-
760
885
  ## Interaction History
761
886
 
762
- Each `RoolSpace` instance has a `conversationId` that tracks interaction history for that space. The history records all meaningful interactions (prompts, object mutations) as self-contained entries, each capturing the request and its result. History is stored in the space data itself and syncs in real-time to all clients.
887
+ Each channel has a `channelId` that identifies it. The history records all meaningful interactions (prompts, object mutations) as self-contained entries, each capturing the request and its result. History is stored in the space data itself and syncs in real-time to all clients.
763
888
 
764
889
  ### What the AI Receives
765
890
 
766
891
  AI operations (`prompt`, `createObject`, `updateObject`, `findObjects`) automatically receive:
767
892
 
768
- - **Interaction history** — Previous interactions and their results from this conversation
893
+ - **Interaction history** — Previous interactions and their results from this channel
769
894
  - **Recently modified objects** — Objects in the space recently created or changed
770
895
  - **Selected objects** — Objects passed via `objectIds` are given primary focus
771
896
 
@@ -774,50 +899,39 @@ This context flows automatically — no configuration needed. The AI sees enough
774
899
  ### Accessing History
775
900
 
776
901
  ```typescript
777
- // Get interactions for the current conversationId
778
- const interactions = space.getInteractions();
902
+ // Get interactions for this channel
903
+ const interactions = channel.getInteractions();
779
904
  // Returns: Interaction[]
780
-
781
- // Get interactions for a specific conversation ID
782
- const interactions = space.getInteractionsById('other-conversation-id');
783
- // Returns: Interaction[]
784
-
785
- // List all conversation IDs that have interactions
786
- const conversationIds = space.getConversationIds();
787
- // Returns: string[]
788
905
  ```
789
906
 
790
- ### Conversation Access Methods
907
+ ### Channel History Methods
791
908
 
792
909
  | Method | Description |
793
910
  |--------|-------------|
794
- | `getInteractions(): Interaction[]` | Get interactions for the current conversationId |
795
- | `getInteractionsById(id): Interaction[]` | Get interactions for a specific conversation ID |
796
- | `getConversationIds(): string[]` | List all conversation IDs that have conversations |
797
- | `deleteConversation(conversationId?): Promise<void>` | Delete a conversation and its history. Defaults to current conversation. |
798
- | `renameConversation(id, name): Promise<void>` | Rename a conversation. Creates it if it doesn't exist. |
799
- | `listConversations(): Promise<ConversationInfo[]>` | List all conversations with summary info. |
800
- | `getSystemInstruction(): string \| undefined` | Get system instruction for current conversation. |
801
- | `setSystemInstruction(instruction): Promise<void>` | Set system instruction for current conversation. Pass `null` to clear. |
911
+ | `getInteractions(): Interaction[]` | Get interactions for this channel |
912
+ | `getSystemInstruction(): string \| undefined` | Get system instruction for this channel |
913
+ | `setSystemInstruction(instruction): Promise<void>` | Set system instruction for this channel. Pass `null` to clear. |
914
+
915
+ Channel management (listing, renaming, deleting channels) is done via the client see [Channel Management](#channel-management).
802
916
 
803
917
  ### System Instructions
804
918
 
805
- System instructions customize how the AI behaves within a conversation. The instruction persists across all prompts in that conversation.
919
+ System instructions customize how the AI behaves within a channel. The instruction persists across all prompts in that channel.
806
920
 
807
921
  ```typescript
808
922
  // Make the AI behave like an SQL interpreter
809
- await space.setSystemInstruction(
923
+ await channel.setSystemInstruction(
810
924
  'Behave like an intelligent SQL interpreter. Respond with simple markdown tables. ' +
811
925
  'Translate the objects in the space to the implied structure in your responses.'
812
926
  );
813
927
 
814
928
  // Now prompts are interpreted as SQL-like queries
815
- const { message } = await space.prompt('SELECT task, due_date FROM tasks ORDER BY due_date');
929
+ const { message } = await channel.prompt('SELECT task, due_date FROM tasks ORDER BY due_date');
816
930
  // Returns a markdown table of tasks, even if no "tasks" objects exist -
817
931
  // the AI infers actual tasks from the space content
818
932
 
819
933
  // Clear the instruction to return to default behavior
820
- await space.setSystemInstruction(null);
934
+ await channel.setSystemInstruction(null);
821
935
  ```
822
936
 
823
937
  System instructions are useful for:
@@ -829,56 +943,37 @@ System instructions are useful for:
829
943
  ### Listening for Updates
830
944
 
831
945
  ```typescript
832
- space.on('conversationUpdated', ({ conversationId, source }) => {
833
- // Conversation changed - refresh if needed
834
- const interactions = space.getInteractions();
946
+ channel.on('channelUpdated', ({ channelId, source }) => {
947
+ // Channel updated - refresh if needed
948
+ const interactions = channel.getInteractions();
835
949
  renderInteractions(interactions);
836
950
  });
837
951
  ```
838
952
 
839
- ### Conversation Isolation
840
-
841
- By default, each call to `openSpace()` or `createSpace()` generates a new `conversationId`. This means:
842
- - Opening a space twice gives you two independent AI conversation histories
843
- - Closing and reopening a space starts fresh
844
-
845
- ### Switching Conversations
953
+ ### Multiple Channels
846
954
 
847
- Set `conversationId` to switch conversations without reopening the space:
955
+ Each channel has its own interaction history. To work with multiple independent histories on the same space, open multiple channels:
848
956
 
849
957
  ```typescript
850
- const space = await client.openSpace('abc123');
851
-
852
- // User clicks "Research" thread in sidebar
853
- space.conversationId = 'research-thread';
854
- await space.prompt("Analyze this data");
855
-
856
- // User clicks "Main" thread
857
- space.conversationId = 'main-thread';
858
- await space.prompt("Summarize findings");
859
-
860
- // Listen for conversation switches
861
- space.on('conversationIdChanged', ({ previousConversationId, newConversationId }) => {
862
- // Re-render chat UI with new conversation's history
863
- renderChat(space.getInteractions());
864
- });
865
- ```
866
-
867
- ### Resuming Conversations
958
+ // Open two channels on the same space
959
+ const research = await client.openChannel('space-id', 'research');
960
+ const main = await client.openChannel('space-id', 'main');
868
961
 
869
- Pass a `conversationId` at open time to start with a specific conversation:
962
+ // Each has independent history
963
+ await research.prompt("Analyze this data");
964
+ await main.prompt("Summarize findings");
870
965
 
871
- ```typescript
872
- // Resume a known conversation when opening
873
- const space = await client.openSpace('abc123', { conversationId: 'research-thread' });
966
+ // Close when done
967
+ research.close();
968
+ main.close();
874
969
  ```
875
970
 
876
971
  **Use cases:**
877
- - **Page refresh** — Store `conversationId` in localStorage to maintain context across reloads
878
- - **Multiple conversations** — Switch between different conversation contexts using the setter
879
- - **Collaborative conversations** — Share a `conversationId` between users to enable shared AI conversation history
972
+ - **Chat app with sidebar** — Each sidebar entry is a channel with a different channelId
973
+ - **Page refresh** — Store the channelId in localStorage to resume the same channel
974
+ - **Collaborative channels** — Share a channelId between users to enable shared AI interaction history
880
975
 
881
- **Tip:** Use the user's id as `conversationId` to share context across tabs/devices, or a fixed string like `'shared'` to share context across all users.
976
+ **Tip:** Use the user's id as channelId to share context across tabs/devices, or a fixed string like `'shared'` to share context across all users.
882
977
 
883
978
  Note: Interaction history is truncated to the most recent 50 entries to manage space size.
884
979
 
@@ -890,11 +985,38 @@ The `ai` field in interactions distinguishes AI-generated responses from synthet
890
985
 
891
986
  ### Tool Calls
892
987
 
893
- The `toolCalls` array captures what the AI agent did during execution. Use it to build responsive UIs that show progress while the agent works — the `conversationUpdated` event fires as each tool completes, letting you display status updates or hints in real-time.
988
+ The `toolCalls` array captures what the AI agent did during execution. Use it to build responsive UIs that show progress while the agent works — the `channelUpdated` event fires as each tool completes, letting you display status updates or hints in real-time.
894
989
 
895
990
  ## Data Types
896
991
 
897
- ### Space Data
992
+ ### Schema Types
993
+
994
+ ```typescript
995
+ // Allowed field types
996
+ type FieldType =
997
+ | { kind: 'string' }
998
+ | { kind: 'number' }
999
+ | { kind: 'boolean' }
1000
+ | { kind: 'array'; inner?: FieldType }
1001
+ | { kind: 'maybe'; inner: FieldType }
1002
+ | { kind: 'enum'; values: string[] }
1003
+ | { kind: 'literal'; value: string | number | boolean }
1004
+ | { kind: 'ref' };
1005
+
1006
+ interface FieldDef {
1007
+ name: string;
1008
+ type: FieldType;
1009
+ }
1010
+
1011
+ interface CollectionDef {
1012
+ fields: FieldDef[];
1013
+ }
1014
+
1015
+ // Full schema — collection names to definitions
1016
+ type SpaceSchema = Record<string, CollectionDef>;
1017
+ ```
1018
+
1019
+ ### Object Data
898
1020
 
899
1021
  ```typescript
900
1022
  // RoolObject represents the object data you work with
@@ -906,25 +1028,29 @@ interface RoolObject {
906
1028
  [key: string]: unknown;
907
1029
  }
908
1030
 
909
- // Object stat - audit information returned by space.stat()
1031
+ // Object stat - audit information returned by channel.stat()
910
1032
  interface RoolObjectStat {
911
1033
  modifiedAt: number;
912
1034
  modifiedBy: string;
913
1035
  modifiedByName: string | null;
914
1036
  }
1037
+ ```
1038
+
1039
+ ### Channels
915
1040
 
916
- // Conversation container with metadata
917
- interface Conversation {
918
- name?: string; // Conversation name (optional)
919
- createdAt: number; // Timestamp when conversation was created
920
- createdBy: string; // User ID who created the conversation
1041
+ ```typescript
1042
+ // Channel container with metadata
1043
+ interface Channel {
1044
+ name?: string; // Channel name (optional)
1045
+ createdAt: number; // Timestamp when channel was created
1046
+ createdBy: string; // User ID who created the channel
921
1047
  createdByName?: string; // Display name at time of creation
922
1048
  systemInstruction?: string; // Custom system instruction for AI
923
1049
  interactions: Interaction[]; // Interaction history
924
1050
  }
925
1051
 
926
- // Conversation summary info (returned by listConversations)
927
- interface ConversationInfo {
1052
+ // Channel summary info (returned by client.getChannels)
1053
+ interface ChannelInfo {
928
1054
  id: string;
929
1055
  name: string | null;
930
1056
  createdAt: number;
@@ -932,24 +1058,10 @@ interface ConversationInfo {
932
1058
  createdByName: string | null;
933
1059
  interactionCount: number;
934
1060
  }
935
-
936
- // Internal space data structure
937
- interface RoolSpaceData {
938
- version: number; // Monotonically increasing version for sync consistency
939
- objects: Record<string, RoolObjectEntry>;
940
- meta: Record<string, unknown>; // Space-level metadata
941
- conversations?: Record<string, Conversation>; // Conversations keyed by conversationId
942
- }
943
-
944
- // Full stored object structure (for advanced use with getData())
945
- interface RoolObjectEntry {
946
- data: RoolObject; // The actual object data
947
- modifiedAt: number; // Timestamp of last modification
948
- modifiedBy: string; // User ID who last modified
949
- modifiedByName: string | null; // Display name at time of modification
950
- }
951
1061
  ```
952
1062
 
1063
+ Note: `Channel` and `ChannelInfo` are data types describing the stored channel metadata. The `Channel` interface is the wire format; `RoolChannel` is the live SDK class you interact with.
1064
+
953
1065
  ### Interaction Types
954
1066
 
955
1067
  ```typescript
@@ -998,7 +1110,7 @@ interface PromptOptions {
998
1110
  objectIds?: string[]; // Scope to specific objects
999
1111
  responseSchema?: Record<string, unknown>;
1000
1112
  effort?: PromptEffort; // Effort level (default: 'STANDARD')
1001
- ephemeral?: boolean; // Don't record in conversation history
1113
+ ephemeral?: boolean; // Don't record in interaction history
1002
1114
  readOnly?: boolean; // Disable mutation tools (default: false)
1003
1115
  attachments?: Array<File | Blob | { data: string; contentType: string }>; // Files to attach (uploaded to media store)
1004
1116
  }
@@ -1009,7 +1121,7 @@ interface PromptOptions {
1009
1121
  A Rool Space is a persistent, shared world model. Applications project different interaction patterns onto the same core primitives:
1010
1122
 
1011
1123
  - **Objects** store durable state, with references to other objects via data fields
1012
- - **Interaction history** tracks what happened (requests, results, modified objects)
1124
+ - **Channels** provide independent AI interaction contexts over shared objects
1013
1125
  - **Events** describe what changed in real-time
1014
1126
 
1015
1127
  Below are a few representative patterns.
@@ -1017,20 +1129,20 @@ Below are a few representative patterns.
1017
1129
  ### Chat With Generated Artifacts
1018
1130
 
1019
1131
  - **Space**: documents, notes, images, tasks as objects
1020
- - **Interaction history**: prompts and AI responses stored in space, synced across clients
1132
+ - **Channels**: each chat thread is a separate channel on the same space
1021
1133
  - **UI**: renders interactions from `getInteractions()` as chat; derives artifact lists from object events
1022
1134
 
1023
1135
  **Pattern**
1024
1136
  - Interaction history syncs in real-time; UI renders entries as chat bubbles
1025
- - Artifacts are persistent objects
1026
- - Listen to `conversationUpdated` to update chat UI
1137
+ - Artifacts are persistent objects shared across all channels
1138
+ - Listen to `channelUpdated` event to update chat UI
1027
1139
  - Selecting objects defines the AI working set via `objectIds`
1028
1140
 
1029
1141
  ### Multi-User World / Text Adventure
1030
1142
 
1031
1143
  - **Space**: rooms, items, NPCs, players as objects
1032
1144
  - **References**: navigation, containment, location via data fields
1033
- - **Conversation**: player commands and narrative continuity
1145
+ - **Channel**: player commands and narrative continuity
1034
1146
 
1035
1147
  **Pattern**
1036
1148
  - The space is the shared world state
@@ -1041,7 +1153,7 @@ Below are a few representative patterns.
1041
1153
 
1042
1154
  - **Space**: concepts, sources, hypotheses as objects
1043
1155
  - **References**: semantic connections between objects via data fields
1044
- - **Conversation**: exploratory analysis and questioning
1156
+ - **Channel**: exploratory analysis and questioning
1045
1157
 
1046
1158
  **Pattern**
1047
1159
  - Graph structure lives in object data fields containing other object IDs
@@ -1052,7 +1164,7 @@ Below are a few representative patterns.
1052
1164
 
1053
1165
  - Durable content lives in space objects
1054
1166
  - References between objects are data fields whose values are object IDs
1055
- - Interaction history lives in space conversations (persistent, synced, truncated to 50 entries)
1167
+ - Interaction history lives in channels (persistent, synced, truncated to 50 entries)
1056
1168
  - UI state lives in the client, space metadata, or `_`-prefixed fields
1057
1169
  - AI focus is controlled by object selection, not by replaying history
1058
1170