@rool-dev/sdk 0.9.0-dev.9cb655b → 0.9.0-dev.bab1e95
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 +258 -128
- package/dist/channel.d.ts +87 -158
- package/dist/channel.d.ts.map +1 -1
- package/dist/channel.js +268 -320
- package/dist/channel.js.map +1 -1
- package/dist/client.d.ts +21 -3
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +31 -3
- package/dist/client.js.map +1 -1
- package/dist/graphql.d.ts +41 -19
- package/dist/graphql.d.ts.map +1 -1
- package/dist/graphql.js +120 -117
- package/dist/graphql.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/locations.d.ts +34 -0
- package/dist/locations.d.ts.map +1 -0
- package/dist/locations.js +90 -0
- package/dist/locations.js.map +1 -0
- package/dist/space.d.ts +1 -1
- package/dist/space.d.ts.map +1 -1
- package/dist/space.js +6 -6
- package/dist/space.js.map +1 -1
- package/dist/subscription.js +4 -2
- package/dist/subscription.js.map +1 -1
- package/dist/types.d.ts +58 -32
- package/dist/types.d.ts.map +1 -1
- package/package.json +1 -1
package/dist/channel.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { EventEmitter } from './event-emitter.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import { generateBasename, loc, normalizeLocation, parseLocation } from './locations.js';
|
|
3
|
+
// 6-character alphanumeric ID — used for interactionIds, conversationIds, etc.
|
|
4
|
+
const ENTITY_CHARS = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz';
|
|
4
5
|
export function generateEntityId() {
|
|
5
6
|
let result = '';
|
|
6
7
|
for (let i = 0; i < 6; i++) {
|
|
7
|
-
result +=
|
|
8
|
+
result += ENTITY_CHARS[Math.floor(Math.random() * ENTITY_CHARS.length)];
|
|
8
9
|
}
|
|
9
10
|
return result;
|
|
10
11
|
}
|
|
@@ -123,16 +124,10 @@ const OBJECT_COLLECT_TIMEOUT = 30000;
|
|
|
123
124
|
* at open time and cannot be changed. To use a different channel,
|
|
124
125
|
* open a second one.
|
|
125
126
|
*
|
|
126
|
-
* Objects are
|
|
127
|
-
*
|
|
127
|
+
* Objects are addressed by location (`/space/<collection>/<basename>.json`).
|
|
128
|
+
* Only schema, metadata, the live object location list, and the channel's own
|
|
129
|
+
* history are cached locally. Object bodies are fetched on demand. Changes
|
|
128
130
|
* arrive via SSE semantic events and are emitted as SDK events.
|
|
129
|
-
*
|
|
130
|
-
* Features:
|
|
131
|
-
* - High-level object operations
|
|
132
|
-
* - Built-in undo/redo with checkpoints
|
|
133
|
-
* - Metadata management
|
|
134
|
-
* - Event emission for state changes
|
|
135
|
-
* - Real-time updates via space-specific subscription
|
|
136
131
|
*/
|
|
137
132
|
export class RoolChannel extends EventEmitter {
|
|
138
133
|
_id;
|
|
@@ -148,20 +143,20 @@ export class RoolChannel extends EventEmitter {
|
|
|
148
143
|
webdav;
|
|
149
144
|
onCloseCallback;
|
|
150
145
|
logger;
|
|
151
|
-
// Local cache for bounded data (schema, metadata, own channel, object
|
|
146
|
+
// Local cache for bounded data (schema, metadata, own channel, object locations, stats)
|
|
152
147
|
_meta;
|
|
153
148
|
_schema;
|
|
154
149
|
_channel;
|
|
155
|
-
|
|
150
|
+
_objectLocations;
|
|
156
151
|
_objectStats;
|
|
157
152
|
// Active leaf per conversation (client-side tree cursor)
|
|
158
153
|
_activeLeaves = new Map();
|
|
159
|
-
// Object collection: tracks pending local mutations for dedup
|
|
160
|
-
// Maps
|
|
154
|
+
// Object collection: tracks pending local mutations (by location) for dedup
|
|
155
|
+
// Maps location → optimistic object (for create/update) or null (for delete)
|
|
161
156
|
_pendingMutations = new Map();
|
|
162
|
-
// Resolvers waiting for object data from SSE events
|
|
157
|
+
// Resolvers waiting for object data from SSE events, keyed by location
|
|
163
158
|
_objectResolvers = new Map();
|
|
164
|
-
// Buffer for object data that arrived before a collector was registered
|
|
159
|
+
// Buffer for object data that arrived before a collector was registered, keyed by location
|
|
165
160
|
_objectBuffer = new Map();
|
|
166
161
|
constructor(config) {
|
|
167
162
|
super();
|
|
@@ -182,7 +177,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
182
177
|
this._meta = config.meta;
|
|
183
178
|
this._schema = config.schema;
|
|
184
179
|
this._channel = config.channel;
|
|
185
|
-
this.
|
|
180
|
+
this._objectLocations = config.objectLocations;
|
|
186
181
|
this._objectStats = new Map(Object.entries(config.objectStats));
|
|
187
182
|
}
|
|
188
183
|
/**
|
|
@@ -203,7 +198,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
203
198
|
return;
|
|
204
199
|
this._meta = data.meta;
|
|
205
200
|
this._schema = data.schema;
|
|
206
|
-
this.
|
|
201
|
+
this._objectLocations = data.objectLocations;
|
|
207
202
|
this._objectStats = new Map(Object.entries(data.objectStats));
|
|
208
203
|
if (data.channel)
|
|
209
204
|
this._channel = data.channel;
|
|
@@ -370,10 +365,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
370
365
|
}
|
|
371
366
|
/**
|
|
372
367
|
* Get a handle for a specific conversation within this channel.
|
|
373
|
-
* The handle scopes AI and mutation operations to that conversation's
|
|
374
|
-
* interaction history, while sharing the channel's single SSE connection.
|
|
375
|
-
*
|
|
376
|
-
* Conversations are auto-created on first interaction — no explicit create needed.
|
|
377
368
|
*/
|
|
378
369
|
conversation(conversationId) {
|
|
379
370
|
return new ConversationHandle(this, conversationId);
|
|
@@ -393,139 +384,113 @@ export class RoolChannel extends EventEmitter {
|
|
|
393
384
|
}
|
|
394
385
|
/**
|
|
395
386
|
* Create a checkpoint of the current space state.
|
|
396
|
-
* Checkpoints are space-wide and shared across channels and users.
|
|
397
|
-
* @returns The checkpoint ID
|
|
398
387
|
*/
|
|
399
388
|
async checkpoint(label = 'Change') {
|
|
400
389
|
const result = await this.graphqlClient.checkpoint(this._id, label, this._channelId);
|
|
401
390
|
return result.checkpointId;
|
|
402
391
|
}
|
|
403
|
-
/**
|
|
404
|
-
* Check if undo is available for this space.
|
|
405
|
-
*/
|
|
392
|
+
/** Check if undo is available for this space. */
|
|
406
393
|
async canUndo() {
|
|
407
394
|
const status = await this.graphqlClient.checkpointStatus(this._id, this._channelId);
|
|
408
395
|
return status.canUndo;
|
|
409
396
|
}
|
|
410
|
-
/**
|
|
411
|
-
* Check if redo is available for this space.
|
|
412
|
-
*/
|
|
397
|
+
/** Check if redo is available for this space. */
|
|
413
398
|
async canRedo() {
|
|
414
399
|
const status = await this.graphqlClient.checkpointStatus(this._id, this._channelId);
|
|
415
400
|
return status.canRedo;
|
|
416
401
|
}
|
|
417
|
-
/**
|
|
418
|
-
* Restore the space to the most recent checkpoint.
|
|
419
|
-
* @returns true if undo was performed
|
|
420
|
-
*/
|
|
402
|
+
/** Restore the space to the most recent checkpoint. */
|
|
421
403
|
async undo() {
|
|
422
404
|
const result = await this.graphqlClient.undo(this._id, this._channelId);
|
|
423
405
|
return result.success;
|
|
424
406
|
}
|
|
425
|
-
/**
|
|
426
|
-
* Reapply the most recently undone checkpoint.
|
|
427
|
-
* Affects the entire space.
|
|
428
|
-
* @returns true if redo was performed
|
|
429
|
-
*/
|
|
407
|
+
/** Reapply the most recently undone checkpoint. */
|
|
430
408
|
async redo() {
|
|
431
409
|
const result = await this.graphqlClient.redo(this._id, this._channelId);
|
|
432
410
|
return result.success;
|
|
433
411
|
}
|
|
434
|
-
/**
|
|
435
|
-
* Clear the space's checkpoint history.
|
|
436
|
-
*/
|
|
412
|
+
/** Clear the space's checkpoint history. */
|
|
437
413
|
async clearHistory() {
|
|
438
414
|
await this.graphqlClient.clearCheckpointHistory(this._id, this._channelId);
|
|
439
415
|
}
|
|
440
416
|
/**
|
|
441
|
-
* Get an object
|
|
442
|
-
*
|
|
417
|
+
* Get an object by location. Fetches from the server on each call.
|
|
418
|
+
*
|
|
419
|
+
* Accepts either the canonical form (`/space/<collection>/<basename>.json`)
|
|
420
|
+
* or the short form (`<collection>/<basename>`).
|
|
443
421
|
*/
|
|
444
|
-
async getObject(
|
|
445
|
-
return this.graphqlClient.getObject(this._id,
|
|
422
|
+
async getObject(location) {
|
|
423
|
+
return this.graphqlClient.getObject(this._id, normalizeLocation(location));
|
|
446
424
|
}
|
|
447
425
|
/**
|
|
448
426
|
* Get an object's stat (audit information).
|
|
449
|
-
* Returns
|
|
427
|
+
* Returns the cached stat or undefined if not known.
|
|
450
428
|
*/
|
|
451
|
-
stat(
|
|
452
|
-
return this._objectStats.get(
|
|
429
|
+
stat(location) {
|
|
430
|
+
return this._objectStats.get(normalizeLocation(location));
|
|
453
431
|
}
|
|
454
432
|
/**
|
|
455
433
|
* Find objects using structured filters and/or natural language.
|
|
456
|
-
*
|
|
457
|
-
* `where` provides exact-match filtering — values must match literally (no placeholders or operators).
|
|
458
|
-
* `prompt` enables AI-powered semantic queries. When both are provided, `where` and `objectIds`
|
|
459
|
-
* constrain the data set before the AI sees it.
|
|
460
|
-
*
|
|
461
|
-
* @param options.where - Exact-match field filter (e.g. `{ type: 'article' }`). Constrains which objects the AI can see when combined with `prompt`.
|
|
462
|
-
* @param options.prompt - Natural language query. Triggers AI evaluation (uses credits).
|
|
463
|
-
* @param options.limit - Maximum number of results to return (applies to structured filtering only; the AI controls its own result size).
|
|
464
|
-
* @param options.objectIds - Scope search to specific object IDs. Constrains the candidate set in both structured and AI queries.
|
|
465
|
-
* @param options.order - Sort order by modifiedAt: `'asc'` or `'desc'` (default: `'desc'`). Only applies to structured filtering (no `prompt`).
|
|
466
|
-
* @param options.ephemeral - If true, the query won't be recorded in interaction history.
|
|
467
|
-
* @returns The matching objects and a descriptive message.
|
|
468
434
|
*/
|
|
469
435
|
async findObjects(options) {
|
|
470
436
|
return this._findObjectsImpl(options, this._conversationId);
|
|
471
437
|
}
|
|
472
438
|
/** @internal */
|
|
473
439
|
_findObjectsImpl(options, conversationId) {
|
|
474
|
-
|
|
440
|
+
const normalized = {
|
|
441
|
+
...options,
|
|
442
|
+
locations: options.locations?.map(normalizeLocation),
|
|
443
|
+
};
|
|
444
|
+
return this.graphqlClient.findObjects(this._id, normalized, this._channelId, conversationId);
|
|
475
445
|
}
|
|
476
446
|
/**
|
|
477
|
-
* Get all object
|
|
447
|
+
* Get all object locations (sync, from local cache).
|
|
478
448
|
* The list is loaded on open and kept current via SSE events.
|
|
479
|
-
* @param options.limit - Maximum number of IDs to return
|
|
480
|
-
* @param options.order - Sort order by modifiedAt ('asc' or 'desc', default: 'desc')
|
|
481
449
|
*/
|
|
482
|
-
|
|
483
|
-
let
|
|
450
|
+
getObjectLocations(options) {
|
|
451
|
+
let locs = this._objectLocations;
|
|
484
452
|
if (options?.order === 'asc') {
|
|
485
|
-
|
|
453
|
+
locs = [...locs].reverse();
|
|
486
454
|
}
|
|
487
455
|
if (options?.limit !== undefined) {
|
|
488
|
-
|
|
456
|
+
locs = locs.slice(0, options.limit);
|
|
489
457
|
}
|
|
490
|
-
return
|
|
458
|
+
return locs;
|
|
491
459
|
}
|
|
492
460
|
/**
|
|
493
|
-
* Create a new object
|
|
494
|
-
*
|
|
461
|
+
* Create a new object in the given collection.
|
|
462
|
+
*
|
|
463
|
+
* @param collection - The collection (must exist in the schema)
|
|
464
|
+
* @param body - Object body fields. Use `{{placeholder}}` for AI-generated content.
|
|
465
|
+
* Fields prefixed with `_` are hidden from AI. Must not contain
|
|
466
|
+
* `id` or `type` (identity lives on the location).
|
|
467
|
+
* @param options.basename - Specific basename to use. If omitted, the SDK generates a random one.
|
|
495
468
|
* @param options.ephemeral - If true, the operation won't be recorded in interaction history.
|
|
496
|
-
* @returns The created object
|
|
469
|
+
* @returns The created object and a status message.
|
|
497
470
|
*/
|
|
498
|
-
async createObject(options) {
|
|
499
|
-
return this._createObjectImpl(options, this._conversationId);
|
|
471
|
+
async createObject(collection, body, options) {
|
|
472
|
+
return this._createObjectImpl(collection, body, options, this._conversationId);
|
|
500
473
|
}
|
|
501
474
|
/** @internal */
|
|
502
|
-
async _createObjectImpl(options, conversationId) {
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
const type = data.type;
|
|
506
|
-
if (!/^[a-zA-Z0-9_-]+$/.test(basename)) {
|
|
507
|
-
throw new Error(`Invalid object ID "${basename}". IDs must contain only alphanumeric characters, hyphens, and underscores.`);
|
|
508
|
-
}
|
|
509
|
-
if (typeof type !== 'string' || !type) {
|
|
510
|
-
throw new Error('createObject: data.type is required');
|
|
475
|
+
async _createObjectImpl(collection, body, options, conversationId) {
|
|
476
|
+
if ('id' in body || 'type' in body) {
|
|
477
|
+
throw new Error('createObject: body must not contain `id` or `type` — identity is the location.');
|
|
511
478
|
}
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
const
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
this._pendingMutations.set(objectId, optimisticObject);
|
|
518
|
-
this.emit('objectCreated', { objectId, object: optimisticObject, source: 'local_user' });
|
|
479
|
+
const basename = options?.basename ?? generateBasename();
|
|
480
|
+
const location = loc(collection, basename);
|
|
481
|
+
const optimistic = { location, collection, basename, body };
|
|
482
|
+
this._pendingMutations.set(location, optimistic);
|
|
483
|
+
this.emit('objectCreated', { location, object: optimistic, source: 'local_user' });
|
|
519
484
|
try {
|
|
520
485
|
const interactionId = generateEntityId();
|
|
521
|
-
const { message } = await this.graphqlClient.createObject(this.
|
|
522
|
-
const
|
|
523
|
-
return { object, message };
|
|
486
|
+
const { message, object } = await this.graphqlClient.createObject(this._id, location, body, this._channelId, conversationId, interactionId, { ephemeral: options?.ephemeral, parentInteractionId: options?.parentInteractionId });
|
|
487
|
+
const fresh = object ?? await this._collectObject(location);
|
|
488
|
+
return { object: fresh, message };
|
|
524
489
|
}
|
|
525
490
|
catch (error) {
|
|
526
491
|
this.logger.error('[RoolChannel] Failed to create object:', error);
|
|
527
|
-
this._pendingMutations.delete(
|
|
528
|
-
this._cancelCollector(
|
|
492
|
+
this._pendingMutations.delete(location);
|
|
493
|
+
this._cancelCollector(location);
|
|
529
494
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
530
495
|
this.emit('reset', { source: 'system' });
|
|
531
496
|
throw error;
|
|
@@ -533,98 +498,141 @@ export class RoolChannel extends EventEmitter {
|
|
|
533
498
|
}
|
|
534
499
|
/**
|
|
535
500
|
* Update an existing object.
|
|
536
|
-
*
|
|
537
|
-
* @param
|
|
538
|
-
* @param options.
|
|
501
|
+
*
|
|
502
|
+
* @param location - The object's location (canonical or short form)
|
|
503
|
+
* @param options.data - Fields to add or update. Pass `null` to delete a field. Use `{{placeholder}}` for AI-generated content.
|
|
504
|
+
* @param options.prompt - AI prompt to drive the update.
|
|
539
505
|
* @param options.ephemeral - If true, the operation won't be recorded in interaction history.
|
|
540
|
-
* @returns The updated object (with AI-filled content) and message
|
|
541
506
|
*/
|
|
542
|
-
async updateObject(
|
|
543
|
-
return this._updateObjectImpl(
|
|
507
|
+
async updateObject(location, options) {
|
|
508
|
+
return this._updateObjectImpl(location, options, this._conversationId);
|
|
544
509
|
}
|
|
545
510
|
/** @internal */
|
|
546
|
-
async _updateObjectImpl(
|
|
547
|
-
const
|
|
548
|
-
|
|
549
|
-
if (data
|
|
550
|
-
throw new Error('
|
|
551
|
-
}
|
|
552
|
-
if (data && ('id' in data)) {
|
|
553
|
-
throw new Error('Cannot delete id field. The id field is immutable after creation.');
|
|
511
|
+
async _updateObjectImpl(location, options, conversationId) {
|
|
512
|
+
const canonical = normalizeLocation(location);
|
|
513
|
+
const { data } = options;
|
|
514
|
+
if (data && ('id' in data || 'type' in data)) {
|
|
515
|
+
throw new Error('updateObject: data must not contain `id` or `type`. Use moveObject to change identity.');
|
|
554
516
|
}
|
|
555
|
-
// Normalize undefined to null (for JSON serialization) and build server
|
|
556
|
-
let
|
|
517
|
+
// Normalize undefined to null (for JSON serialization) and build server patch
|
|
518
|
+
let serverPatch;
|
|
557
519
|
if (data) {
|
|
558
|
-
|
|
520
|
+
serverPatch = {};
|
|
559
521
|
for (const [key, value] of Object.entries(data)) {
|
|
560
|
-
|
|
561
|
-
serverData[key] = value === undefined ? null : value;
|
|
522
|
+
serverPatch[key] = value === undefined ? null : value;
|
|
562
523
|
}
|
|
563
524
|
}
|
|
564
525
|
// Emit optimistic event if we have data changes
|
|
565
526
|
if (data) {
|
|
566
|
-
|
|
567
|
-
const optimistic = {
|
|
568
|
-
this._pendingMutations.set(
|
|
569
|
-
this.emit('objectUpdated', {
|
|
527
|
+
const { collection, basename } = parseLocation(canonical);
|
|
528
|
+
const optimistic = { location: canonical, collection, basename, body: data };
|
|
529
|
+
this._pendingMutations.set(canonical, optimistic);
|
|
530
|
+
this.emit('objectUpdated', { location: canonical, object: optimistic, source: 'local_user' });
|
|
570
531
|
}
|
|
571
532
|
try {
|
|
572
533
|
const interactionId = generateEntityId();
|
|
573
|
-
const { message } = await this.graphqlClient.updateObject(this.
|
|
574
|
-
|
|
575
|
-
|
|
534
|
+
const { message, object } = await this.graphqlClient.updateObject(this._id, canonical, this._channelId, conversationId, interactionId, {
|
|
535
|
+
patch: serverPatch,
|
|
536
|
+
prompt: options.prompt,
|
|
537
|
+
ephemeral: options.ephemeral,
|
|
538
|
+
parentInteractionId: options.parentInteractionId,
|
|
539
|
+
});
|
|
540
|
+
const fresh = object ?? await this._collectObject(canonical);
|
|
541
|
+
return { object: fresh, message };
|
|
576
542
|
}
|
|
577
543
|
catch (error) {
|
|
578
544
|
this.logger.error('[RoolChannel] Failed to update object:', error);
|
|
579
|
-
this._pendingMutations.delete(
|
|
580
|
-
this._cancelCollector(
|
|
545
|
+
this._pendingMutations.delete(canonical);
|
|
546
|
+
this._cancelCollector(canonical);
|
|
581
547
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
582
548
|
this.emit('reset', { source: 'system' });
|
|
583
549
|
throw error;
|
|
584
550
|
}
|
|
585
551
|
}
|
|
586
552
|
/**
|
|
587
|
-
*
|
|
588
|
-
*
|
|
553
|
+
* Move (rename or relocate) an object to a new location.
|
|
554
|
+
* Use this to rename, change collection, or atomically rewrite the body.
|
|
555
|
+
*
|
|
556
|
+
* @param from - Current location
|
|
557
|
+
* @param to - New location
|
|
558
|
+
* @param options.body - Replace the body atomically as part of the move.
|
|
559
|
+
* @param options.ephemeral - If true, the operation won't be recorded in interaction history.
|
|
589
560
|
*/
|
|
590
|
-
async
|
|
591
|
-
return this.
|
|
561
|
+
async moveObject(from, to, options) {
|
|
562
|
+
return this._moveObjectImpl(from, to, options, this._conversationId);
|
|
592
563
|
}
|
|
593
564
|
/** @internal */
|
|
594
|
-
async
|
|
595
|
-
|
|
565
|
+
async _moveObjectImpl(from, to, options, conversationId) {
|
|
566
|
+
const fromLoc = normalizeLocation(from);
|
|
567
|
+
const toLoc = normalizeLocation(to);
|
|
568
|
+
if (options?.body && ('id' in options.body || 'type' in options.body)) {
|
|
569
|
+
throw new Error('moveObject: body must not contain `id` or `type` — identity is the location.');
|
|
570
|
+
}
|
|
571
|
+
// Optimistic event — emit move so listeners can update keys
|
|
572
|
+
const { collection, basename } = parseLocation(toLoc);
|
|
573
|
+
const optimistic = {
|
|
574
|
+
location: toLoc,
|
|
575
|
+
collection,
|
|
576
|
+
basename,
|
|
577
|
+
body: options?.body ?? {},
|
|
578
|
+
};
|
|
579
|
+
this._pendingMutations.set(toLoc, optimistic);
|
|
580
|
+
this.emit('objectMoved', { from: fromLoc, to: toLoc, object: optimistic, source: 'local_user' });
|
|
581
|
+
try {
|
|
582
|
+
const interactionId = generateEntityId();
|
|
583
|
+
const { message, object } = await this.graphqlClient.moveObject(this._id, fromLoc, toLoc, this._channelId, conversationId, interactionId, {
|
|
584
|
+
body: options?.body,
|
|
585
|
+
ephemeral: options?.ephemeral,
|
|
586
|
+
parentInteractionId: options?.parentInteractionId,
|
|
587
|
+
});
|
|
588
|
+
const fresh = object ?? await this._collectObject(toLoc);
|
|
589
|
+
return { object: fresh, message };
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
this.logger.error('[RoolChannel] Failed to move object:', error);
|
|
593
|
+
this._pendingMutations.delete(toLoc);
|
|
594
|
+
this._cancelCollector(toLoc);
|
|
595
|
+
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
596
|
+
this.emit('reset', { source: 'system' });
|
|
597
|
+
throw error;
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Delete objects by location.
|
|
602
|
+
* Other objects that reference deleted objects will retain stale ref values.
|
|
603
|
+
*/
|
|
604
|
+
async deleteObjects(locations) {
|
|
605
|
+
return this._deleteObjectsImpl(locations, this._conversationId);
|
|
606
|
+
}
|
|
607
|
+
/** @internal */
|
|
608
|
+
async _deleteObjectsImpl(locations, conversationId) {
|
|
609
|
+
if (locations.length === 0)
|
|
596
610
|
return;
|
|
611
|
+
const canonical = locations.map(normalizeLocation);
|
|
597
612
|
// Track for dedup and emit optimistic events
|
|
598
|
-
for (const
|
|
599
|
-
this._pendingMutations.set(
|
|
600
|
-
this.emit('objectDeleted', {
|
|
613
|
+
for (const location of canonical) {
|
|
614
|
+
this._pendingMutations.set(location, null);
|
|
615
|
+
this.emit('objectDeleted', { location, source: 'local_user' });
|
|
601
616
|
}
|
|
602
617
|
try {
|
|
603
|
-
|
|
618
|
+
const interactionId = generateEntityId();
|
|
619
|
+
await this.graphqlClient.deleteObjects(this._id, canonical, this._channelId, conversationId, interactionId);
|
|
604
620
|
}
|
|
605
621
|
catch (error) {
|
|
606
622
|
this.logger.error('[RoolChannel] Failed to delete objects:', error);
|
|
607
|
-
for (const
|
|
608
|
-
this._pendingMutations.delete(
|
|
623
|
+
for (const location of canonical) {
|
|
624
|
+
this._pendingMutations.delete(location);
|
|
609
625
|
}
|
|
610
626
|
this.emit('syncError', error instanceof Error ? error : new Error(String(error)));
|
|
611
627
|
this.emit('reset', { source: 'system' });
|
|
612
628
|
throw error;
|
|
613
629
|
}
|
|
614
630
|
}
|
|
615
|
-
/**
|
|
616
|
-
* Get the current schema for this space.
|
|
617
|
-
* Returns a map of collection names to their definitions.
|
|
618
|
-
*/
|
|
631
|
+
/** Get the current schema for this space. */
|
|
619
632
|
getSchema() {
|
|
620
633
|
return this._schema;
|
|
621
634
|
}
|
|
622
|
-
/**
|
|
623
|
-
* Create a new collection schema.
|
|
624
|
-
* @param name - Collection name (must start with a letter, alphanumeric/hyphens/underscores only)
|
|
625
|
-
* @param fields - Field definitions for the collection
|
|
626
|
-
* @returns The created CollectionDef
|
|
627
|
-
*/
|
|
635
|
+
/** Create a new collection schema. */
|
|
628
636
|
async createCollection(name, fields) {
|
|
629
637
|
return this._createCollectionImpl(name, fields, this._conversationId);
|
|
630
638
|
}
|
|
@@ -645,12 +653,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
645
653
|
throw error;
|
|
646
654
|
}
|
|
647
655
|
}
|
|
648
|
-
/**
|
|
649
|
-
* Alter an existing collection schema, replacing its field definitions.
|
|
650
|
-
* @param name - Name of the collection to alter
|
|
651
|
-
* @param fields - New field definitions (replaces all existing fields)
|
|
652
|
-
* @returns The updated CollectionDef
|
|
653
|
-
*/
|
|
656
|
+
/** Alter an existing collection schema, replacing its field definitions. */
|
|
654
657
|
async alterCollection(name, fields) {
|
|
655
658
|
return this._alterCollectionImpl(name, fields, this._conversationId);
|
|
656
659
|
}
|
|
@@ -660,7 +663,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
660
663
|
throw new Error(`Collection "${name}" not found`);
|
|
661
664
|
}
|
|
662
665
|
const previous = this._schema[name];
|
|
663
|
-
// Optimistic local update
|
|
664
666
|
this._schema[name] = { fields: fields.map(f => ({ name: f.name, type: f.type })) };
|
|
665
667
|
try {
|
|
666
668
|
return await this.graphqlClient.alterCollection(this._id, name, fields, this._channelId, conversationId);
|
|
@@ -671,10 +673,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
671
673
|
throw error;
|
|
672
674
|
}
|
|
673
675
|
}
|
|
674
|
-
/**
|
|
675
|
-
* Drop a collection schema.
|
|
676
|
-
* @param name - Name of the collection to drop
|
|
677
|
-
*/
|
|
676
|
+
/** Drop a collection schema. */
|
|
678
677
|
async dropCollection(name) {
|
|
679
678
|
return this._dropCollectionImpl(name, this._conversationId);
|
|
680
679
|
}
|
|
@@ -684,7 +683,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
684
683
|
throw new Error(`Collection "${name}" not found`);
|
|
685
684
|
}
|
|
686
685
|
const previous = this._schema[name];
|
|
687
|
-
// Optimistic local update
|
|
688
686
|
delete this._schema[name];
|
|
689
687
|
try {
|
|
690
688
|
await this.graphqlClient.dropCollection(this._id, name, this._channelId, conversationId);
|
|
@@ -697,7 +695,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
697
695
|
}
|
|
698
696
|
/**
|
|
699
697
|
* Get the system instruction for the current conversation.
|
|
700
|
-
* Returns undefined if no system instruction is set.
|
|
701
698
|
*/
|
|
702
699
|
getSystemInstruction() {
|
|
703
700
|
return this._getSystemInstructionImpl(this._conversationId);
|
|
@@ -706,16 +703,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
706
703
|
_getSystemInstructionImpl(conversationId) {
|
|
707
704
|
return this._channel?.conversations[conversationId]?.systemInstruction;
|
|
708
705
|
}
|
|
709
|
-
/**
|
|
710
|
-
* Set the system instruction for the current conversation.
|
|
711
|
-
* Pass null to clear the instruction.
|
|
712
|
-
*/
|
|
706
|
+
/** Set the system instruction for the current conversation. */
|
|
713
707
|
async setSystemInstruction(instruction) {
|
|
714
708
|
return this._setSystemInstructionImpl(instruction, this._conversationId);
|
|
715
709
|
}
|
|
716
710
|
/** @internal */
|
|
717
711
|
async _setSystemInstructionImpl(instruction, conversationId) {
|
|
718
|
-
// Optimistic local update
|
|
719
712
|
this._ensureConversationImpl(conversationId);
|
|
720
713
|
const conv = this._channel.conversations[conversationId];
|
|
721
714
|
const previousInstruction = conv.systemInstruction;
|
|
@@ -725,7 +718,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
725
718
|
else {
|
|
726
719
|
conv.systemInstruction = instruction;
|
|
727
720
|
}
|
|
728
|
-
// Emit events for backward compat and new API
|
|
729
721
|
this.emit('conversationUpdated', {
|
|
730
722
|
conversationId,
|
|
731
723
|
channelId: this._channelId,
|
|
@@ -737,13 +729,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
737
729
|
source: 'local_user',
|
|
738
730
|
});
|
|
739
731
|
}
|
|
740
|
-
// Call server
|
|
741
732
|
try {
|
|
742
733
|
await this.graphqlClient.updateConversation(this._id, this._channelId, conversationId, { systemInstruction: instruction });
|
|
743
734
|
}
|
|
744
735
|
catch (error) {
|
|
745
736
|
this.logger.error('[RoolChannel] Failed to set system instruction:', error);
|
|
746
|
-
// Rollback
|
|
747
737
|
if (previousInstruction === undefined) {
|
|
748
738
|
delete conv.systemInstruction;
|
|
749
739
|
}
|
|
@@ -753,15 +743,12 @@ export class RoolChannel extends EventEmitter {
|
|
|
753
743
|
throw error;
|
|
754
744
|
}
|
|
755
745
|
}
|
|
756
|
-
/**
|
|
757
|
-
* Rename the current conversation.
|
|
758
|
-
*/
|
|
746
|
+
/** Rename the current conversation. */
|
|
759
747
|
async renameConversation(name) {
|
|
760
748
|
return this._renameConversationImpl(name, this._conversationId);
|
|
761
749
|
}
|
|
762
750
|
/** @internal */
|
|
763
751
|
async _renameConversationImpl(name, conversationId) {
|
|
764
|
-
// Optimistic local update
|
|
765
752
|
this._ensureConversationImpl(conversationId);
|
|
766
753
|
const conv = this._channel.conversations[conversationId];
|
|
767
754
|
const previousName = conv.name;
|
|
@@ -777,21 +764,16 @@ export class RoolChannel extends EventEmitter {
|
|
|
777
764
|
source: 'local_user',
|
|
778
765
|
});
|
|
779
766
|
}
|
|
780
|
-
// Call server
|
|
781
767
|
try {
|
|
782
768
|
await this.graphqlClient.updateConversation(this._id, this._channelId, conversationId, { name });
|
|
783
769
|
}
|
|
784
770
|
catch (error) {
|
|
785
771
|
this.logger.error('[RoolChannel] Failed to rename conversation:', error);
|
|
786
|
-
// Rollback
|
|
787
772
|
conv.name = previousName;
|
|
788
773
|
throw error;
|
|
789
774
|
}
|
|
790
775
|
}
|
|
791
|
-
/**
|
|
792
|
-
* Ensure a conversation exists in the local channel cache.
|
|
793
|
-
* @internal
|
|
794
|
-
*/
|
|
776
|
+
/** @internal */
|
|
795
777
|
_ensureConversationImpl(conversationId) {
|
|
796
778
|
if (!this._channel) {
|
|
797
779
|
this._channel = {
|
|
@@ -808,10 +790,7 @@ export class RoolChannel extends EventEmitter {
|
|
|
808
790
|
};
|
|
809
791
|
}
|
|
810
792
|
}
|
|
811
|
-
/**
|
|
812
|
-
* Set a space-level metadata value.
|
|
813
|
-
* Metadata is stored in meta and hidden from AI operations.
|
|
814
|
-
*/
|
|
793
|
+
/** Set a space-level metadata value. */
|
|
815
794
|
setMetadata(key, value) {
|
|
816
795
|
this._setMetadataImpl(key, value, this._conversationId);
|
|
817
796
|
}
|
|
@@ -820,38 +799,33 @@ export class RoolChannel extends EventEmitter {
|
|
|
820
799
|
this._meta[key] = value;
|
|
821
800
|
this.emit('metadataUpdated', { metadata: this._meta, source: 'local_user' });
|
|
822
801
|
// Fire-and-forget server call
|
|
823
|
-
this.graphqlClient.setSpaceMeta(this.
|
|
802
|
+
this.graphqlClient.setSpaceMeta(this._id, this._meta, this._channelId, conversationId)
|
|
824
803
|
.catch((error) => {
|
|
825
804
|
this.logger.error('[RoolChannel] Failed to set meta:', error);
|
|
826
805
|
});
|
|
827
806
|
}
|
|
828
|
-
/**
|
|
829
|
-
* Get a space-level metadata value.
|
|
830
|
-
*/
|
|
807
|
+
/** Get a space-level metadata value. */
|
|
831
808
|
getMetadata(key) {
|
|
832
809
|
return this._meta[key];
|
|
833
810
|
}
|
|
834
|
-
/**
|
|
835
|
-
* Get all space-level metadata.
|
|
836
|
-
*/
|
|
811
|
+
/** Get all space-level metadata. */
|
|
837
812
|
getAllMetadata() {
|
|
838
813
|
return this._meta;
|
|
839
814
|
}
|
|
840
815
|
/**
|
|
841
816
|
* Send a prompt to the AI agent for space manipulation.
|
|
842
|
-
* @returns The message from the AI and the list of objects that were created or modified
|
|
817
|
+
* @returns The message from the AI and the list of objects that were created or modified.
|
|
843
818
|
*/
|
|
844
819
|
async prompt(prompt, options) {
|
|
845
820
|
return this._promptImpl(prompt, options, this._conversationId);
|
|
846
821
|
}
|
|
847
822
|
/** @internal */
|
|
848
823
|
async _promptImpl(prompt, options, conversationId) {
|
|
849
|
-
|
|
850
|
-
const { attachments, parentInteractionId: explicitParent, signal, ...rest } = options ?? {};
|
|
824
|
+
const { attachments, parentInteractionId: explicitParent, signal, locations, ...rest } = options ?? {};
|
|
851
825
|
const interactionId = generateEntityId();
|
|
852
|
-
let
|
|
826
|
+
let attachmentRefs;
|
|
853
827
|
if (attachments?.length) {
|
|
854
|
-
|
|
828
|
+
attachmentRefs = await Promise.all(attachments.map((file, index) => this.uploadAttachment(file, interactionId, index)));
|
|
855
829
|
}
|
|
856
830
|
// Auto-continue from active leaf if no explicit parent provided
|
|
857
831
|
const parentInteractionId = explicitParent !== undefined
|
|
@@ -862,8 +836,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
862
836
|
let onAbort;
|
|
863
837
|
if (signal) {
|
|
864
838
|
if (signal.aborted) {
|
|
865
|
-
// Caller aborted before we even started; fire-and-forget the stop so
|
|
866
|
-
// the server-side prompt (about to start) is cancelled too.
|
|
867
839
|
this.graphqlClient.stopInteraction(this._id, interactionId).catch(() => { });
|
|
868
840
|
}
|
|
869
841
|
else {
|
|
@@ -875,29 +847,33 @@ export class RoolChannel extends EventEmitter {
|
|
|
875
847
|
}
|
|
876
848
|
let result;
|
|
877
849
|
try {
|
|
878
|
-
result = await this.graphqlClient.prompt(this._id, prompt, this._channelId, conversationId, {
|
|
850
|
+
result = await this.graphqlClient.prompt(this._id, prompt, this._channelId, conversationId, {
|
|
851
|
+
...rest,
|
|
852
|
+
locations: locations?.map(normalizeLocation),
|
|
853
|
+
attachmentRefs,
|
|
854
|
+
interactionId,
|
|
855
|
+
parentInteractionId,
|
|
856
|
+
});
|
|
879
857
|
}
|
|
880
858
|
finally {
|
|
881
859
|
if (onAbort)
|
|
882
860
|
signal.removeEventListener('abort', onAbort);
|
|
883
861
|
}
|
|
884
862
|
// Collect modified objects — they arrive via SSE events during/after the mutation.
|
|
885
|
-
// Try collecting from buffer first, then fetch any missing from server.
|
|
886
863
|
const objects = [];
|
|
887
864
|
const missing = [];
|
|
888
|
-
for (const
|
|
889
|
-
const buffered = this._objectBuffer.get(
|
|
865
|
+
for (const location of result.modifiedObjectLocations) {
|
|
866
|
+
const buffered = this._objectBuffer.get(location);
|
|
890
867
|
if (buffered) {
|
|
891
|
-
this._objectBuffer.delete(
|
|
868
|
+
this._objectBuffer.delete(location);
|
|
892
869
|
objects.push(buffered);
|
|
893
870
|
}
|
|
894
871
|
else {
|
|
895
|
-
missing.push(
|
|
872
|
+
missing.push(location);
|
|
896
873
|
}
|
|
897
874
|
}
|
|
898
|
-
// Fetch any objects not yet received via SSE
|
|
899
875
|
if (missing.length > 0) {
|
|
900
|
-
const fetched = await Promise.all(missing.map(
|
|
876
|
+
const fetched = await Promise.all(missing.map(location => this.graphqlClient.getObject(this._id, location)));
|
|
901
877
|
for (const obj of fetched) {
|
|
902
878
|
if (obj)
|
|
903
879
|
objects.push(obj);
|
|
@@ -908,23 +884,18 @@ export class RoolChannel extends EventEmitter {
|
|
|
908
884
|
objects,
|
|
909
885
|
};
|
|
910
886
|
}
|
|
911
|
-
/**
|
|
912
|
-
* Rename this channel.
|
|
913
|
-
*/
|
|
887
|
+
/** Rename this channel. */
|
|
914
888
|
async rename(newName) {
|
|
915
|
-
// Optimistic local update
|
|
916
889
|
const previousName = this._channel?.name;
|
|
917
890
|
if (this._channel) {
|
|
918
891
|
this._channel.name = newName;
|
|
919
892
|
}
|
|
920
893
|
this.emit('channelUpdated', { channelId: this._channelId, source: 'local_user' });
|
|
921
|
-
// Call server
|
|
922
894
|
try {
|
|
923
895
|
await this.graphqlClient.renameChannel(this._id, this._channelId, newName);
|
|
924
896
|
}
|
|
925
897
|
catch (error) {
|
|
926
898
|
this.logger.error('[RoolChannel] Failed to rename channel:', error);
|
|
927
|
-
// Rollback
|
|
928
899
|
if (this._channel) {
|
|
929
900
|
this._channel.name = previousName;
|
|
930
901
|
}
|
|
@@ -933,11 +904,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
933
904
|
}
|
|
934
905
|
/**
|
|
935
906
|
* Fetch an external URL via the server proxy, bypassing CORS restrictions.
|
|
936
|
-
* Requires editor role or above. Blocked for private/internal IP ranges (SSRF protection).
|
|
937
|
-
*
|
|
938
|
-
* @param url - The URL to fetch
|
|
939
|
-
* @param init - Optional method, headers, and body
|
|
940
|
-
* @returns The proxied Response
|
|
941
907
|
*/
|
|
942
908
|
async fetch(url, init) {
|
|
943
909
|
return this.restClient.proxyFetch(this._id, url, init);
|
|
@@ -959,57 +925,48 @@ export class RoolChannel extends EventEmitter {
|
|
|
959
925
|
}
|
|
960
926
|
/**
|
|
961
927
|
* Register a collector that resolves when the object arrives via SSE.
|
|
962
|
-
* If the object is already in the buffer (arrived before collector), resolves immediately.
|
|
963
928
|
* @internal
|
|
964
929
|
*/
|
|
965
|
-
_collectObject(
|
|
930
|
+
_collectObject(location) {
|
|
966
931
|
return new Promise((resolve, reject) => {
|
|
967
|
-
|
|
968
|
-
const buffered = this._objectBuffer.get(objectId);
|
|
932
|
+
const buffered = this._objectBuffer.get(location);
|
|
969
933
|
if (buffered) {
|
|
970
|
-
this._objectBuffer.delete(
|
|
934
|
+
this._objectBuffer.delete(location);
|
|
971
935
|
resolve(buffered);
|
|
972
936
|
return;
|
|
973
937
|
}
|
|
974
938
|
const timer = setTimeout(() => {
|
|
975
|
-
this._objectResolvers.delete(
|
|
939
|
+
this._objectResolvers.delete(location);
|
|
976
940
|
// Fallback: try to fetch from server
|
|
977
|
-
this.graphqlClient.getObject(this._id,
|
|
941
|
+
this.graphqlClient.getObject(this._id, location).then(obj => {
|
|
978
942
|
if (obj) {
|
|
979
943
|
resolve(obj);
|
|
980
944
|
}
|
|
981
945
|
else {
|
|
982
|
-
reject(new Error(`Timeout waiting for object ${
|
|
946
|
+
reject(new Error(`Timeout waiting for object ${location} from SSE`));
|
|
983
947
|
}
|
|
984
948
|
}).catch(reject);
|
|
985
949
|
}, OBJECT_COLLECT_TIMEOUT);
|
|
986
|
-
this._objectResolvers.set(
|
|
950
|
+
this._objectResolvers.set(location, (obj) => {
|
|
987
951
|
clearTimeout(timer);
|
|
988
952
|
resolve(obj);
|
|
989
953
|
});
|
|
990
954
|
});
|
|
991
955
|
}
|
|
992
|
-
/**
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
_cancelCollector(objectId) {
|
|
997
|
-
this._objectResolvers.delete(objectId);
|
|
998
|
-
this._objectBuffer.delete(objectId);
|
|
956
|
+
/** @internal */
|
|
957
|
+
_cancelCollector(location) {
|
|
958
|
+
this._objectResolvers.delete(location);
|
|
959
|
+
this._objectBuffer.delete(location);
|
|
999
960
|
}
|
|
1000
|
-
/**
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
*/
|
|
1004
|
-
_deliverObject(objectId, object) {
|
|
1005
|
-
const resolver = this._objectResolvers.get(objectId);
|
|
961
|
+
/** @internal */
|
|
962
|
+
_deliverObject(location, object) {
|
|
963
|
+
const resolver = this._objectResolvers.get(location);
|
|
1006
964
|
if (resolver) {
|
|
1007
965
|
resolver(object);
|
|
1008
|
-
this._objectResolvers.delete(
|
|
966
|
+
this._objectResolvers.delete(location);
|
|
1009
967
|
}
|
|
1010
968
|
else {
|
|
1011
|
-
|
|
1012
|
-
this._objectBuffer.set(objectId, object);
|
|
969
|
+
this._objectBuffer.set(location, object);
|
|
1013
970
|
}
|
|
1014
971
|
}
|
|
1015
972
|
/**
|
|
@@ -1017,7 +974,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
1017
974
|
* @internal
|
|
1018
975
|
*/
|
|
1019
976
|
handleChannelEvent(event) {
|
|
1020
|
-
// Ignore events after close - the channel is being torn down
|
|
1021
977
|
if (this._closed)
|
|
1022
978
|
return;
|
|
1023
979
|
const changeSource = event.source === 'agent' ? 'remote_agent' : 'remote_user';
|
|
@@ -1026,23 +982,31 @@ export class RoolChannel extends EventEmitter {
|
|
|
1026
982
|
// Resync is handled by the client via _applyResyncData.
|
|
1027
983
|
break;
|
|
1028
984
|
case 'object_created':
|
|
1029
|
-
if (event.
|
|
985
|
+
if (event.location && event.object) {
|
|
1030
986
|
if (event.objectStat)
|
|
1031
|
-
this._objectStats.set(event.
|
|
1032
|
-
this._handleObjectCreated(event.
|
|
987
|
+
this._objectStats.set(event.location, event.objectStat);
|
|
988
|
+
this._handleObjectCreated(event.location, event.object, changeSource);
|
|
1033
989
|
}
|
|
1034
990
|
break;
|
|
1035
991
|
case 'object_updated':
|
|
1036
|
-
if (event.
|
|
992
|
+
if (event.location && event.object) {
|
|
1037
993
|
if (event.objectStat)
|
|
1038
|
-
this._objectStats.set(event.
|
|
1039
|
-
this._handleObjectUpdated(event.
|
|
994
|
+
this._objectStats.set(event.location, event.objectStat);
|
|
995
|
+
this._handleObjectUpdated(event.location, event.object, changeSource);
|
|
1040
996
|
}
|
|
1041
997
|
break;
|
|
1042
998
|
case 'object_deleted':
|
|
1043
|
-
if (event.
|
|
1044
|
-
this._objectStats.delete(event.
|
|
1045
|
-
this._handleObjectDeleted(event.
|
|
999
|
+
if (event.location) {
|
|
1000
|
+
this._objectStats.delete(event.location);
|
|
1001
|
+
this._handleObjectDeleted(event.location, changeSource);
|
|
1002
|
+
}
|
|
1003
|
+
break;
|
|
1004
|
+
case 'object_moved':
|
|
1005
|
+
if (event.from && event.to && event.object) {
|
|
1006
|
+
this._objectStats.delete(event.from);
|
|
1007
|
+
if (event.objectStat)
|
|
1008
|
+
this._objectStats.set(event.to, event.objectStat);
|
|
1009
|
+
this._handleObjectMoved(event.from, event.to, event.object, changeSource);
|
|
1046
1010
|
}
|
|
1047
1011
|
break;
|
|
1048
1012
|
case 'schema_updated':
|
|
@@ -1058,7 +1022,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
1058
1022
|
}
|
|
1059
1023
|
break;
|
|
1060
1024
|
case 'channel_updated':
|
|
1061
|
-
// Only update if it's our channel — channel_updated is now metadata-only (name, extensionUrl)
|
|
1062
1025
|
if (event.channelId === this._channelId && event.channel) {
|
|
1063
1026
|
const changed = JSON.stringify(this._channel) !== JSON.stringify(event.channel);
|
|
1064
1027
|
this._channel = event.channel;
|
|
@@ -1068,7 +1031,6 @@ export class RoolChannel extends EventEmitter {
|
|
|
1068
1031
|
}
|
|
1069
1032
|
break;
|
|
1070
1033
|
case 'conversation_updated':
|
|
1071
|
-
// Only update if it's our channel
|
|
1072
1034
|
if (event.channelId === this._channelId && event.conversationId) {
|
|
1073
1035
|
if (!this._channel) {
|
|
1074
1036
|
this._channel = {
|
|
@@ -1079,17 +1041,13 @@ export class RoolChannel extends EventEmitter {
|
|
|
1079
1041
|
}
|
|
1080
1042
|
const prev = this._channel.conversations[event.conversationId];
|
|
1081
1043
|
if (event.conversation) {
|
|
1082
|
-
// Update or create conversation in local cache
|
|
1083
1044
|
this._channel.conversations[event.conversationId] = event.conversation;
|
|
1084
1045
|
}
|
|
1085
1046
|
else {
|
|
1086
|
-
// Conversation was deleted
|
|
1087
1047
|
delete this._channel.conversations[event.conversationId];
|
|
1088
1048
|
}
|
|
1089
|
-
// Skip emit if data is unchanged (e.g. echo of our own optimistic update)
|
|
1090
1049
|
if (JSON.stringify(prev) === JSON.stringify(event.conversation))
|
|
1091
1050
|
break;
|
|
1092
|
-
// Auto-advance active leaf if someone continued our current branch
|
|
1093
1051
|
if (event.conversation && !Array.isArray(event.conversation.interactions)) {
|
|
1094
1052
|
const currentLeaf = this._getActiveLeafImpl(event.conversationId);
|
|
1095
1053
|
if (currentLeaf) {
|
|
@@ -1101,13 +1059,11 @@ export class RoolChannel extends EventEmitter {
|
|
|
1101
1059
|
}
|
|
1102
1060
|
}
|
|
1103
1061
|
}
|
|
1104
|
-
// Emit the new conversationUpdated event
|
|
1105
1062
|
this.emit('conversationUpdated', {
|
|
1106
1063
|
conversationId: event.conversationId,
|
|
1107
1064
|
channelId: event.channelId,
|
|
1108
1065
|
source: changeSource,
|
|
1109
1066
|
});
|
|
1110
|
-
// Backward compat: also emit channelUpdated when the active conversation updates
|
|
1111
1067
|
if (event.conversationId === this._conversationId) {
|
|
1112
1068
|
this.emit('channelUpdated', { channelId: event.channelId, source: changeSource });
|
|
1113
1069
|
}
|
|
@@ -1118,87 +1074,75 @@ export class RoolChannel extends EventEmitter {
|
|
|
1118
1074
|
break;
|
|
1119
1075
|
}
|
|
1120
1076
|
}
|
|
1121
|
-
/**
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
// Deliver to any pending collector (for mutation return values)
|
|
1128
|
-
this._deliverObject(objectId, object);
|
|
1129
|
-
// Maintain local ID list — prepend (most recently modified first)
|
|
1130
|
-
this._objectIds = [objectId, ...this._objectIds.filter(id => id !== objectId)];
|
|
1131
|
-
const pending = this._pendingMutations.get(objectId);
|
|
1077
|
+
/** @internal */
|
|
1078
|
+
_handleObjectCreated(location, object, source) {
|
|
1079
|
+
this._deliverObject(location, object);
|
|
1080
|
+
// Maintain local location list — prepend (most recently modified first)
|
|
1081
|
+
this._objectLocations = [location, ...this._objectLocations.filter(l => l !== location)];
|
|
1082
|
+
const pending = this._pendingMutations.get(location);
|
|
1132
1083
|
if (pending !== undefined) {
|
|
1133
|
-
|
|
1134
|
-
this._pendingMutations.delete(objectId);
|
|
1084
|
+
this._pendingMutations.delete(location);
|
|
1135
1085
|
if (pending !== null) {
|
|
1136
|
-
//
|
|
1086
|
+
// Already emitted objectCreated optimistically.
|
|
1137
1087
|
// Emit objectUpdated only if AI resolved placeholders (data changed).
|
|
1138
1088
|
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1139
|
-
this.emit('objectUpdated', {
|
|
1089
|
+
this.emit('objectUpdated', { location, object, source });
|
|
1140
1090
|
}
|
|
1141
1091
|
}
|
|
1142
1092
|
}
|
|
1143
1093
|
else {
|
|
1144
|
-
|
|
1145
|
-
this.emit('objectCreated', { objectId, object, source });
|
|
1094
|
+
this.emit('objectCreated', { location, object, source });
|
|
1146
1095
|
}
|
|
1147
1096
|
}
|
|
1148
|
-
/**
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
_handleObjectUpdated(objectId, object, source) {
|
|
1154
|
-
// Deliver to any pending collector
|
|
1155
|
-
this._deliverObject(objectId, object);
|
|
1156
|
-
// Maintain local ID list — move to front (most recently modified)
|
|
1157
|
-
this._objectIds = [objectId, ...this._objectIds.filter(id => id !== objectId)];
|
|
1158
|
-
const pending = this._pendingMutations.get(objectId);
|
|
1097
|
+
/** @internal */
|
|
1098
|
+
_handleObjectUpdated(location, object, source) {
|
|
1099
|
+
this._deliverObject(location, object);
|
|
1100
|
+
this._objectLocations = [location, ...this._objectLocations.filter(l => l !== location)];
|
|
1101
|
+
const pending = this._pendingMutations.get(location);
|
|
1159
1102
|
if (pending !== undefined) {
|
|
1160
|
-
|
|
1161
|
-
this._pendingMutations.delete(objectId);
|
|
1103
|
+
this._pendingMutations.delete(location);
|
|
1162
1104
|
if (pending !== null) {
|
|
1163
|
-
// Already emitted objectUpdated optimistically.
|
|
1164
|
-
// Emit again only if data changed (AI resolved placeholders).
|
|
1165
1105
|
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1166
|
-
this.emit('objectUpdated', {
|
|
1106
|
+
this.emit('objectUpdated', { location, object, source });
|
|
1167
1107
|
}
|
|
1168
1108
|
}
|
|
1169
1109
|
}
|
|
1170
1110
|
else {
|
|
1171
|
-
|
|
1172
|
-
this.emit('objectUpdated', { objectId, object, source });
|
|
1111
|
+
this.emit('objectUpdated', { location, object, source });
|
|
1173
1112
|
}
|
|
1174
1113
|
}
|
|
1175
|
-
/**
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
*/
|
|
1180
|
-
_handleObjectDeleted(objectId, source) {
|
|
1181
|
-
// Remove from local ID list
|
|
1182
|
-
this._objectIds = this._objectIds.filter(id => id !== objectId);
|
|
1183
|
-
const pending = this._pendingMutations.get(objectId);
|
|
1114
|
+
/** @internal */
|
|
1115
|
+
_handleObjectDeleted(location, source) {
|
|
1116
|
+
this._objectLocations = this._objectLocations.filter(l => l !== location);
|
|
1117
|
+
const pending = this._pendingMutations.get(location);
|
|
1184
1118
|
if (pending !== undefined) {
|
|
1185
|
-
|
|
1186
|
-
this._pendingMutations.delete(objectId);
|
|
1119
|
+
this._pendingMutations.delete(location);
|
|
1187
1120
|
}
|
|
1188
1121
|
else {
|
|
1189
|
-
|
|
1190
|
-
|
|
1122
|
+
this.emit('objectDeleted', { location, source });
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
/** @internal */
|
|
1126
|
+
_handleObjectMoved(from, to, object, source) {
|
|
1127
|
+
this._deliverObject(to, object);
|
|
1128
|
+
// Drop old location, insert new one at the front.
|
|
1129
|
+
this._objectLocations = [to, ...this._objectLocations.filter(l => l !== from && l !== to)];
|
|
1130
|
+
const pending = this._pendingMutations.get(to);
|
|
1131
|
+
if (pending !== undefined) {
|
|
1132
|
+
this._pendingMutations.delete(to);
|
|
1133
|
+
if (pending !== null) {
|
|
1134
|
+
if (JSON.stringify(pending) !== JSON.stringify(object)) {
|
|
1135
|
+
this.emit('objectUpdated', { location: to, object, source });
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
else {
|
|
1140
|
+
this.emit('objectMoved', { from, to, object, source });
|
|
1191
1141
|
}
|
|
1192
1142
|
}
|
|
1193
1143
|
}
|
|
1194
1144
|
/**
|
|
1195
1145
|
* A lightweight handle for a specific conversation within a channel.
|
|
1196
|
-
*
|
|
1197
|
-
* Scopes AI and mutation operations to a particular conversation's interaction
|
|
1198
|
-
* history, while sharing the channel's single SSE connection and object state.
|
|
1199
|
-
*
|
|
1200
|
-
* Obtain via `channel.conversation('thread-id')`.
|
|
1201
|
-
* Conversations are auto-created on first interaction.
|
|
1202
1146
|
*/
|
|
1203
1147
|
export class ConversationHandle {
|
|
1204
1148
|
/** @internal */
|
|
@@ -1244,16 +1188,20 @@ export class ConversationHandle {
|
|
|
1244
1188
|
return this._channel._findObjectsImpl(options, this._conversationId);
|
|
1245
1189
|
}
|
|
1246
1190
|
/** Create a new object. */
|
|
1247
|
-
async createObject(options) {
|
|
1248
|
-
return this._channel._createObjectImpl(options, this._conversationId);
|
|
1191
|
+
async createObject(collection, body, options) {
|
|
1192
|
+
return this._channel._createObjectImpl(collection, body, options, this._conversationId);
|
|
1249
1193
|
}
|
|
1250
1194
|
/** Update an existing object. */
|
|
1251
|
-
async updateObject(
|
|
1252
|
-
return this._channel._updateObjectImpl(
|
|
1195
|
+
async updateObject(location, options) {
|
|
1196
|
+
return this._channel._updateObjectImpl(location, options, this._conversationId);
|
|
1197
|
+
}
|
|
1198
|
+
/** Move (rename/relocate) an object. */
|
|
1199
|
+
async moveObject(from, to, options) {
|
|
1200
|
+
return this._channel._moveObjectImpl(from, to, options, this._conversationId);
|
|
1253
1201
|
}
|
|
1254
|
-
/** Delete objects by
|
|
1255
|
-
async deleteObjects(
|
|
1256
|
-
return this._channel._deleteObjectsImpl(
|
|
1202
|
+
/** Delete objects by location. */
|
|
1203
|
+
async deleteObjects(locations) {
|
|
1204
|
+
return this._channel._deleteObjectsImpl(locations, this._conversationId);
|
|
1257
1205
|
}
|
|
1258
1206
|
/** Send a prompt to the AI agent, scoped to this conversation's history. */
|
|
1259
1207
|
async prompt(text, options) {
|