@asaidimu/utils-workspace 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.mts CHANGED
@@ -1,5 +1,17 @@
1
1
  import { QueryFilter, PaginationOptions } from '@asaidimu/query';
2
- import { SchemaDefinition, SchemaChange, DataTransform } from '@asaidimu/anansi';
2
+ import { IndexDefinition, SchemaDefinition, SchemaChange, DataTransform } from '@asaidimu/anansi';
3
+ import { EventBus } from '@asaidimu/events';
4
+
5
+ /**
6
+ * Utility type for representing partial updates to the state, allowing deep nesting.
7
+ * It makes all properties optional and applies the same transformation recursively
8
+ * to nested objects and array elements, allowing for selective updates while
9
+ * preserving the original structure. It also includes the original type T and
10
+ * undefined as possibilities for the top level and nested values.
11
+ */
12
+ type DeepPartial<T> = T extends object ? T extends readonly (infer U)[] ? readonly (DeepPartial<U> | undefined)[] | undefined | T : T extends (infer U)[] ? (DeepPartial<U> | undefined)[] | undefined | T : {
13
+ [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> | undefined : T[K] | undefined;
14
+ } | undefined | T : T | undefined;
3
15
 
4
16
  type UUID = string;
5
17
  type Timestamp = string;
@@ -12,35 +24,38 @@ type Result<T, E = WorkspaceError> = {
12
24
  ok: false;
13
25
  error: E;
14
26
  };
15
- type WorkspaceError = {
27
+ type DuplicateKeyError = {
16
28
  code: 'DUPLICATE_KEY';
17
29
  resource: string;
18
30
  key: string;
19
- } | {
31
+ };
32
+ type NotFoundError = {
20
33
  code: 'NOT_FOUND';
21
34
  resource: string;
22
35
  id: string;
23
- } | {
36
+ };
37
+ type InvalidCommandError = {
24
38
  code: 'INVALID_COMMAND';
25
39
  reason: string;
26
- } | {
40
+ };
41
+ type BackendError = {
27
42
  code: 'BACKEND_ERROR';
28
43
  reason: string;
29
- } | {
44
+ };
45
+ type BlobError = {
30
46
  code: 'BLOB_ERROR';
31
47
  reason: string;
32
48
  };
49
+ type WorkspaceError = DuplicateKeyError | NotFoundError | InvalidCommandError | BackendError | BlobError;
33
50
  interface Settings {
34
51
  language: string;
35
52
  defaultRole?: string;
36
53
  prompt?: string;
37
54
  }
38
- interface Project {
55
+ type Project<Metadata extends Record<string, any> = Record<string, any>> = Metadata & {
56
+ id: UUID;
39
57
  name: string;
40
- owner: string;
41
- repository?: string;
42
- metadata?: Record<string, any>;
43
- }
58
+ };
44
59
  type ImageMediaType = 'image/jpeg' | 'image/png' | 'image/gif' | 'image/webp';
45
60
  type DocumentMediaType = 'application/pdf' | 'application/json' | 'text/plain' | 'text/html' | 'text/markdown' | 'text/csv' | 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' | 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
46
61
  type BlobMediaType = ImageMediaType | DocumentMediaType;
@@ -78,59 +93,104 @@ interface BlobRecord {
78
93
  lastUsedAt: Timestamp;
79
94
  }
80
95
  interface TextBlock {
96
+ id: UUID;
81
97
  type: 'text';
82
98
  text: string;
83
99
  }
84
100
  interface ImageBlock {
101
+ id: UUID;
85
102
  type: 'image';
86
103
  ref?: BlobRef;
87
104
  blob?: ResolvedBlob;
88
105
  altText?: string;
89
106
  }
90
107
  interface DocumentBlock {
108
+ id: UUID;
91
109
  type: 'document';
92
110
  ref?: BlobRef;
93
111
  blob?: ResolvedBlob;
94
112
  title?: string;
95
113
  }
114
+ interface TaskProposalBlock {
115
+ id: UUID;
116
+ taskId?: UUID;
117
+ type: 'task:proposal';
118
+ title: string;
119
+ steps: {
120
+ id?: UUID;
121
+ text: string;
122
+ status: 'todo' | 'done';
123
+ }[];
124
+ action: 'create' | 'update' | 'complete';
125
+ }
96
126
  interface ToolUseBlock {
97
- type: 'tool_use';
98
- toolUseId: string;
127
+ id: UUID;
128
+ type: 'tool:use';
99
129
  name: string;
100
130
  input: Record<string, unknown>;
101
131
  }
102
132
  interface ToolResultBlock {
103
- type: 'tool_result';
104
- toolUseId: string;
133
+ id: UUID;
134
+ type: 'tool:result';
135
+ useId: UUID;
105
136
  content: string | Record<string, unknown>;
106
137
  isError?: boolean;
107
138
  }
108
139
  interface ThinkingBlock {
140
+ id: UUID;
109
141
  type: 'thinking';
110
142
  thinking: string;
111
143
  }
112
144
  interface SummaryBlock {
145
+ id: UUID;
113
146
  type: 'summary';
114
147
  text: string;
115
148
  }
116
149
  interface RoleTransitionBlock {
117
- type: 'role_transition';
150
+ id: UUID;
151
+ type: 'role:transition';
118
152
  previousRole: string | null;
119
153
  newRole: string;
120
154
  }
121
- type ContentBlock = TextBlock | ImageBlock | DocumentBlock | ToolUseBlock | ToolResultBlock | SummaryBlock | RoleTransitionBlock | ThinkingBlock;
155
+ type ContentBlock = TextBlock | ImageBlock | DocumentBlock | ToolUseBlock | ToolResultBlock | SummaryBlock | RoleTransitionBlock | ThinkingBlock | TaskProposalBlock;
156
+ interface ToolSummary {
157
+ name: string;
158
+ description: string;
159
+ parameters: {
160
+ type: 'object';
161
+ properties: Record<string, any>;
162
+ required: string[];
163
+ };
164
+ /**
165
+ * The semantic tags for this tool.
166
+ * Used by the ContextRetriever to filter relevance.
167
+ */
168
+ topics: string[];
169
+ }
170
+ interface ToolCall {
171
+ id: UUID;
172
+ tool: UUID;
173
+ arguments: Record<string, any>;
174
+ }
175
+ type AuthRequest = {
176
+ type: 'command';
177
+ payload: Command;
178
+ } | {
179
+ type: 'tool';
180
+ payload: ToolCall;
181
+ };
122
182
  type TurnRole = 'user' | 'assistant' | 'tool';
123
183
  interface Turn {
124
184
  id: UUID;
125
185
  version: number;
126
- role: TurnRole;
186
+ owner: TurnRole;
127
187
  blocks: ContentBlock[];
128
188
  timestamp: Timestamp;
129
189
  /**
130
190
  * The name of the role active when this turn was recorded.
131
191
  * Snapshot so history is stable even if the role is later renamed.
132
192
  */
133
- roleSnapshot?: string;
193
+ role?: string;
134
194
  /**
135
195
  * Parent pointer. Null for root turns.
136
196
  * This is plain data stored on the document — the DAG is reconstructed
@@ -224,6 +284,24 @@ interface SessionMeta {
224
284
  id: UUID;
225
285
  version: number;
226
286
  } | null;
287
+ task: UUID | null;
288
+ }
289
+ type TaskStatus = 'todo' | 'active' | 'done' | 'blocked' | 'defered';
290
+ interface TaskStep {
291
+ id: UUID;
292
+ text: string;
293
+ completed?: Timestamp;
294
+ }
295
+ interface Task {
296
+ id: UUID;
297
+ title: string;
298
+ description?: string;
299
+ status: TaskStatus;
300
+ steps: TaskStep[];
301
+ topics: string[];
302
+ metadata?: Record<string, any>;
303
+ created: Timestamp;
304
+ updated: Timestamp;
227
305
  }
228
306
  interface RoleSummary {
229
307
  name: string;
@@ -248,10 +326,21 @@ interface ContextSummary {
248
326
  source?: string;
249
327
  metadata?: Record<string, any>;
250
328
  }
329
+ interface TaskSummary {
330
+ id: UUID;
331
+ title: string;
332
+ status: TaskStatus;
333
+ steps: {
334
+ completed: number;
335
+ total: number;
336
+ };
337
+ topics: string[];
338
+ }
251
339
  interface TopicIndex {
252
340
  topic: string;
253
341
  contextKeys: string[];
254
342
  preferences: UUID[];
343
+ tasks: UUID[];
255
344
  metadata?: {
256
345
  created?: Timestamp;
257
346
  updated?: Timestamp;
@@ -263,13 +352,15 @@ interface Index {
263
352
  preferences: Record<UUID, PreferenceSummary>;
264
353
  context: Record<string, ContextSummary>;
265
354
  sessions: Record<UUID, SessionMeta>;
355
+ tasks: Record<UUID, TaskSummary>;
266
356
  topics: Record<string, TopicIndex>;
267
357
  blobs: Record<SHA256, BlobRecord>;
358
+ tools: Record<string, ToolSummary>;
268
359
  }
269
- interface Workspace {
360
+ interface Workspace<ProjectMetadata extends Record<string, any> = Record<string, any>> {
270
361
  id: UUID;
271
362
  settings: Settings;
272
- project: Project;
363
+ project: Project<ProjectMetadata>;
273
364
  index: Index;
274
365
  }
275
366
  interface WorkspaceBundle {
@@ -281,6 +372,7 @@ interface WorkspaceBundle {
281
372
  sessions: Record<UUID, SessionMeta & {
282
373
  turns: Turn[];
283
374
  }>;
375
+ tasks: Record<UUID, Task>;
284
376
  blobs: Record<SHA256, BlobRecord>;
285
377
  }
286
378
  interface EffectiveSession {
@@ -289,12 +381,14 @@ interface EffectiveSession {
289
381
  preferences: Preference[];
290
382
  context: Context[];
291
383
  transcript: Turn[];
384
+ task: Task | null;
292
385
  instructions?: string;
293
386
  }
294
387
  interface CacheConfig {
295
388
  roles?: number;
296
389
  preferences?: number;
297
390
  context?: number;
391
+ tasks?: number;
298
392
  }
299
393
  interface FlushConfig {
300
394
  maxBufferSize: number;
@@ -375,6 +469,29 @@ interface DeleteContext extends BaseCommand {
375
469
  key: string;
376
470
  };
377
471
  }
472
+ interface AddTask extends BaseCommand {
473
+ type: 'task:add';
474
+ payload: Task;
475
+ }
476
+ interface UpdateTask extends BaseCommand {
477
+ type: 'task:update';
478
+ payload: Partial<Task> & {
479
+ id: UUID;
480
+ };
481
+ }
482
+ interface DeleteTask extends BaseCommand {
483
+ type: 'task:delete';
484
+ payload: {
485
+ id: UUID;
486
+ };
487
+ }
488
+ interface SaveTurn extends BaseCommand {
489
+ type: 'turn:save';
490
+ payload: {
491
+ sessionId: UUID;
492
+ turn: Turn;
493
+ };
494
+ }
378
495
  interface AddTurn extends BaseCommand {
379
496
  type: 'turn:add';
380
497
  payload: {
@@ -396,21 +513,20 @@ interface BranchTurn extends BaseCommand {
396
513
  type: 'turn:branch';
397
514
  payload: {
398
515
  sessionId: UUID;
399
- parentTurnId: UUID;
400
- parentVersion: number;
401
- newTurn: Turn;
516
+ turn: Turn;
402
517
  };
403
518
  }
519
+ interface TurnRef {
520
+ id: UUID;
521
+ version: number;
522
+ }
404
523
  interface DeleteTurn extends BaseCommand {
405
524
  type: 'turn:delete';
406
525
  payload: {
407
526
  sessionId: UUID;
408
527
  turnId: UUID;
409
528
  version: number;
410
- newHead: {
411
- id: UUID;
412
- version: number;
413
- } | null;
529
+ newHead: TurnRef | null;
414
530
  };
415
531
  }
416
532
  interface SwitchRole extends BaseCommand {
@@ -485,7 +601,11 @@ interface RecordBlobRemoteId extends BaseCommand {
485
601
  };
486
602
  }
487
603
  type BlobCommand = RegisterBlob | RetainBlob | ReleaseBlob | PurgeBlob | RecordBlobRemoteId;
488
- type Command = CreateWorkspace | AddRole | UpdateRole | DeleteRole | AddPreference | UpdatePreference | DeletePreference | CreateSession | AddContext | UpdateContext | DeleteContext | AddTurn | EditTurn | BranchTurn | DeleteTurn | SwitchRole | AddSessionTopics | OverrideSessionPreferences | ForkSession | DeleteSession | BlobCommand;
604
+ interface CallTool extends BaseCommand {
605
+ type: 'tool:call';
606
+ payload: ToolCall;
607
+ }
608
+ type Command = CreateWorkspace | AddRole | UpdateRole | DeleteRole | AddPreference | UpdatePreference | DeletePreference | CreateSession | AddContext | UpdateContext | DeleteContext | AddTask | UpdateTask | DeleteTask | AddTurn | SaveTurn | EditTurn | BranchTurn | DeleteTurn | SwitchRole | AddSessionTopics | OverrideSessionPreferences | ForkSession | DeleteSession | BlobCommand | CallTool;
489
609
  interface TokenBudget {
490
610
  total: number;
491
611
  estimator?: (text: string) => number;
@@ -514,6 +634,7 @@ interface Prompt {
514
634
  persona: string;
515
635
  preferences: Preference[];
516
636
  context: Context[];
637
+ task: Task | null;
517
638
  };
518
639
  context: Context[];
519
640
  transcript: {
@@ -538,15 +659,86 @@ interface TranscriptWindow {
538
659
  flushedCount: number;
539
660
  hasMore: boolean;
540
661
  }
662
+ interface WorkspaceEvents {
663
+ 'workspace:changed': DeepPartial<Workspace>;
664
+ 'blobs:changed': {
665
+ sha256: SHA256;
666
+ record: BlobRecord | null;
667
+ };
668
+ }
541
669
 
670
+ /**
671
+ * Buffers write operations across one or more stores and commits them atomically.
672
+ *
673
+ * ## How atomicity works
674
+ *
675
+ * ### IndexedDB stores (same database)
676
+ * At commit time, TransactionContext collects the names of every IDB store that
677
+ * received operations, then opens a **single** `IDBTransaction` spanning all of
678
+ * them via `ConnectionManager.openTransaction`. Each store's `executeInTransaction`
679
+ * receives that shared transaction object and performs its writes against it
680
+ * without opening a new transaction of its own. IDB commits or aborts the
681
+ * entire multi-store transaction as one unit.
682
+ *
683
+ * ### MemoryStore
684
+ * MemoryStore's `executeInTransaction` receives `null` for the shared transaction.
685
+ * It applies ops against an internal staging map and returns. If a later
686
+ * participant fails, TransactionContext calls `rollbackMemory` on each
687
+ * MemoryStore that already applied its staged ops. MemoryStore restores its
688
+ * pre-transaction snapshot.
689
+ *
690
+ * ### Mixed (IDB + Memory in the same transaction)
691
+ * All IDB stores are committed first as a single atomic IDB transaction, then
692
+ * each MemoryStore is committed. If a MemoryStore fails after IDB has already
693
+ * committed, the IDB side cannot be rolled back — this is an inherent limitation
694
+ * of mixing two different storage engines. In practice the schema store is
695
+ * always MemoryStore-or-IDB consistently, so mixed transactions should not arise
696
+ * in normal usage.
697
+ */
542
698
  declare class TransactionContext {
543
699
  readonly id: string;
544
- private buffer;
545
- private committed;
700
+ /**
701
+ * Flat list of every operation staged so far, in the order they were added.
702
+ * We keep the store reference alongside the op so commit() can group them.
703
+ */
704
+ private staged;
705
+ private done;
546
706
  constructor();
547
- addOp(store: Store<any>, type: "put" | "delete", data: any): void;
707
+ /**
708
+ * Stages a single write operation against a store.
709
+ * Does NOT touch the store — no I/O happens until commit().
710
+ */
711
+ addOp<T extends Record<string, any>>(store: Store<T>, type: "put" | "delete" | "add", data: any): Promise<void>;
712
+ /**
713
+ * Commits all staged operations atomically.
714
+ *
715
+ * For IDB stores: opens one shared IDBTransaction across all participating
716
+ * stores, then dispatches ops to each store's executeInTransaction.
717
+ * For MemoryStores: dispatches sequentially; rolls back on failure.
718
+ */
548
719
  commit(): Promise<void>;
720
+ /**
721
+ * Discards all staged operations. No I/O has occurred so there is nothing
722
+ * to undo — we simply clear the buffer.
723
+ */
549
724
  rollback(): void;
725
+ /**
726
+ * Opens ONE IDBTransaction across all participating IDB stores and lets
727
+ * each store execute its ops against the shared transaction handle.
728
+ *
729
+ * We obtain the IDBDatabase from the first store (they all share the same
730
+ * ConnectionManager / database) and open the transaction ourselves so that
731
+ * the commit/abort lifecycle belongs entirely to this method.
732
+ */
733
+ private commitIDB;
734
+ /**
735
+ * Commits MemoryStore groups sequentially.
736
+ * Maintains a list of stores that have already applied their ops; if any
737
+ * store throws, all previously-applied stores are rolled back via the
738
+ * store-level `_rollbackMemory(snapshot)` escape hatch.
739
+ */
740
+ private commitMemory;
741
+ completed(): boolean;
550
742
  }
551
743
 
552
744
  interface CursorCallbackResult<T> {
@@ -560,8 +752,9 @@ interface CursorCallbackResult<T> {
560
752
  * @template T - The type of records stored.
561
753
  * @param value - The current record value (cloned, not a live reference).
562
754
  * @param key - The key (ID) of the current record.
563
- * @param cursor - The underlying cursor object (implementationspecific; may be `null` in memory adapters).
564
- * @returns A promise that resolves to an object indicating whether iteration should stop, and an optional offset to advance.
755
+ * @param cursor - The underlying cursor object (implementation-specific; may be `null` in memory adapters).
756
+ * @returns A promise resolving to an object indicating whether iteration should stop,
757
+ * and an optional offset to advance.
565
758
  */
566
759
  type CursorCallback<T> = (value: T, key: string | number, cursor: any) => Promise<CursorCallbackResult<T>>;
567
760
  /**
@@ -573,123 +766,100 @@ interface StoreKeyRange {
573
766
  lowerOpen?: boolean;
574
767
  upperOpen?: boolean;
575
768
  }
769
+ /**
770
+ * A single buffered operation staged inside a TransactionContext.
771
+ * Kept intentionally minimal — the context only needs to know what to
772
+ * replay against a store during commit.
773
+ */
774
+ type BufferedOperation<T> = {
775
+ type: "add" | "put";
776
+ data: T | T[];
777
+ } | {
778
+ type: "delete";
779
+ data: string | number | (string | number)[];
780
+ };
576
781
  /**
577
782
  * Storage adapter interface for a single object store (collection).
578
783
  *
579
- * This interface abstracts all low‑level persistence operations, allowing
580
- * different backends (IndexedDB, memory, remote, etc.) to be used interchangeably.
581
- * All methods return promises and operate on clones of data to prevent
582
- * unintended mutations.
784
+ * Stores own their indexes. Index lifecycle (create, drop) and index-aware reads
785
+ * (findByIndex) are part of this contract so that both MemoryStore and IndexedDBStore
786
+ * implement them natively MemoryStore via in-memory index maps, IndexedDB via its
787
+ * native index mechanism.
583
788
  *
584
- * @template T - The type of objects stored in this store. Must include the key path property.
789
+ * @template T - The type of objects stored. Must include the key path property.
585
790
  */
586
- interface Store<T = any> {
791
+ interface Store<T extends Record<string, any> = Record<string, any>> {
792
+ /**
793
+ * Returns the name of this store (the IDB object store / collection name).
794
+ * Used by TransactionContext to group operations and open a correctly-scoped
795
+ * multi-store IDB transaction at commit time.
796
+ */
797
+ name(): string;
798
+ /**
799
+ * Opens the store, ensuring underlying storage structures exist.
800
+ */
801
+ open(): Promise<void>;
587
802
  /**
588
803
  * Adds one or more records to the store.
589
- *
590
804
  * If a record does not have a value for the store's key path, an automatic
591
- * key (e.g., auto‑incremented number) may be assigned. The store's key path
592
- * property is then updated on the added record(s).
593
- *
594
- * @param data - A single record or an array of records to add.
595
- * @returns A promise that resolves to:
596
- * - the key(s) of the added record(s) – a single key if `data` was a single record,
597
- * or an array of keys if `data` was an array.
598
- * @throws {Error} If any record lacks the key path property and auto‑keying is not supported,
599
- * or if a record with the same key already exists.
805
+ * key may be assigned. Throws if a record with the same key already exists.
600
806
  */
601
807
  add(data: T | T[]): Promise<string | number | (string | number)[]>;
602
808
  /**
603
- * Removes all records from the store.
604
- *
605
- * @returns A promise that resolves when the store is cleared.
606
- * @throws {Error} If the operation fails (e.g., store is closed).
809
+ * Removes all records from the store without destroying index structures.
607
810
  */
608
811
  clear(): Promise<void>;
609
812
  /**
610
813
  * Returns the total number of records in the store.
611
- *
612
- * @returns A promise that resolves to the record count.
613
- * @throws {Error} If the operation fails.
614
814
  */
615
815
  count(): Promise<number>;
616
816
  /**
617
817
  * Deletes one or more records by their keys.
618
- *
619
- * @param id - A single key or an array of keys to delete.
620
- * @returns A promise that resolves when the records are deleted.
621
- * @throws {Error} If any key is `undefined` or the operation fails.
622
818
  */
623
819
  delete(id: string | number | (string | number)[]): Promise<void>;
624
820
  /**
625
821
  * Retrieves a single record by its primary key.
626
- *
627
- * @param id - The key of the record to retrieve.
628
- * @returns A promise that resolves to the record (cloned) if found, otherwise `undefined`.
629
- * @throws {Error} If the key is `undefined` or the operation fails.
630
822
  */
631
823
  getById(id: string | number): Promise<T | undefined>;
632
824
  /**
633
- * Retrieves a single record by an index and index key.
825
+ * Retrieves the first record matching an exact index key (point lookup).
826
+ * Useful for unique indexes — returns the single matching record or undefined.
634
827
  *
635
- * @param index - The name of the index to query.
636
- * @param key - The exact key value to look up in the index.
637
- * @returns A promise that resolves to the first matching record (cloned), or `undefined` if none.
638
- * @throws {Error} If the index does not exist, the key is `undefined`, or the operation fails.
828
+ * @param indexName - The name of the index to query.
829
+ * @param key - The exact key value to look up.
639
830
  */
640
- getByIndex(index: string, key: any): Promise<T | undefined>;
831
+ getByIndex(indexName: string, key: any): Promise<T | undefined>;
641
832
  /**
642
- * Retrieves multiple records from an index, optionally within a key range.
833
+ * Retrieves all records from a named index, optionally filtered by a key range.
834
+ * Use this for range scans over an index (e.g. all records where age >= 18).
643
835
  *
644
- * @param index - The name of the index to query.
645
- * @param keyRange - Optional `StoreKeyRange` to filter results. If omitted, all records are returned.
646
- * @returns A promise that resolves to an array of matching records (each cloned).
647
- * @throws {Error} If the index does not exist or the operation fails.
836
+ * @param indexName - The name of the index to query.
837
+ * @param keyRange - Optional range to filter results.
648
838
  */
649
- getByKeyRange(index: string, keyRange?: StoreKeyRange): Promise<T[]>;
839
+ getByKeyRange(indexName: string, keyRange?: StoreKeyRange): Promise<T[]>;
650
840
  /**
651
- * Retrieves all records from the store.
652
- *
653
- * @returns A promise that resolves to an array of all records (each cloned).
654
- * @throws {Error} If the operation fails.
841
+ * Retrieves all records from the store without index involvement.
655
842
  */
656
843
  getAll(): Promise<T[]>;
657
844
  /**
658
- * Inserts or replaces a record.
659
- *
660
- * If a record with the same key already exists, it is replaced.
661
- * The record must contain the store's key path property.
662
- *
663
- * @param data - The record to store.
664
- * @returns A promise that resolves to the key of the stored record.
665
- * @throws {Error} If the record lacks the key path property or the operation fails.
845
+ * Inserts or replaces a record. Validates OCC if a record with the same key exists.
666
846
  */
667
847
  put(data: T): Promise<string | number>;
668
848
  /**
669
849
  * Iterates over records using a cursor, allowing early termination and skipping.
670
850
  *
671
- * The callback is invoked for each record in iteration order. The callback
672
- * can control the iteration by returning `{ done: true }` to stop, or
673
- * `{ offset: n }` to skip ahead `n` records.
674
- *
675
- * @param callback - Function called for each record.
676
- * @param direction - Iteration direction: `"forward"` (ascending keys) or `"backward"` (descending keys).
677
- * @param keyRange - An optional StoreKeyRange to start from specific points.
678
- * @returns A promise that resolves to the last record processed (or `null` if none).
679
- * @throws {Error} If the callback throws or the operation fails.
851
+ * @param callback - Invoked for each record; return `{ done: true }` to stop,
852
+ * `{ offset: n }` to skip ahead n records.
853
+ * @param direction - Iteration order.
854
+ * @param keyRange - Optional range to restrict iteration.
680
855
  */
681
856
  cursor(callback: CursorCallback<T>, direction?: "forward" | "backward", keyRange?: StoreKeyRange): Promise<T | null>;
682
857
  /**
683
- * Executes a batch of write operations atomically.
684
- *
685
- * All operations in the batch succeed or fail together. This is useful for
686
- * maintaining consistency when multiple writes are required.
858
+ * Executes a batch of write operations atomically within this store.
859
+ * All operations succeed or fail together.
687
860
  *
688
- * @param operations - An array of operations. Each operation can be:
689
- * - `{ type: "add" | "put", data: T | T[] }`
690
- * - `{ type: "delete", data: string | number | (string | number)[] }`
691
- * @returns A promise that resolves when the batch is committed.
692
- * @throws {Error} If any operation fails or the batch cannot be completed.
861
+ * Used for standalone (single-store) atomic writes. For cross-store atomicity,
862
+ * use executeInTransaction instead.
693
863
  */
694
864
  batch(operations: Array<{
695
865
  type: "add" | "put";
@@ -698,44 +868,81 @@ interface Store<T = any> {
698
868
  type: "delete";
699
869
  data: string | number | (string | number)[];
700
870
  }>): Promise<void>;
701
- open(): Promise<void>;
871
+ /**
872
+ * Registers a new index on the store. Idempotent — no-op if the index already exists.
873
+ * For IndexedDB, this triggers a database version upgrade.
874
+ * For MemoryStore, this builds the index map from existing records.
875
+ *
876
+ * @param definition - The full index definition from the schema.
877
+ */
878
+ createIndex(definition: IndexDefinition): Promise<void>;
879
+ /**
880
+ * Removes a named index from the store.
881
+ * For IndexedDB, this triggers a database version upgrade.
882
+ * For MemoryStore, this drops the in-memory index map.
883
+ *
884
+ * @param name - The index name as declared in IndexDefinition.name.
885
+ */
886
+ dropIndex(name: string): Promise<void>;
887
+ /**
888
+ * Returns all records matching an exact index key.
889
+ * Unlike getByIndex (which returns only the first match), this returns all matches —
890
+ * essential for non-unique indexes where multiple records share the same indexed value.
891
+ *
892
+ * @param indexName - The name of the index to query.
893
+ * @param value - The exact value to look up.
894
+ */
895
+ findByIndex(indexName: string, value: any): Promise<T[]>;
896
+ /**
897
+ * Executes a set of buffered operations as part of a cross-store atomic transaction.
898
+ *
899
+ * For IndexedDBStore: `sharedTx` is the single IDBTransaction opened across all
900
+ * participating stores. Operations are applied directly to `sharedTx.objectStore(name)`
901
+ * without opening a new transaction — IDB commits or aborts the whole thing atomically.
902
+ *
903
+ * For MemoryStore: `sharedTx` is null. The store applies ops against its own staging
904
+ * area. The caller (TransactionContext) is responsible for coordinating rollback across
905
+ * all MemoryStores if any participant fails.
906
+ *
907
+ * This method must NOT open, commit, or abort any transaction itself.
908
+ *
909
+ * @param ops - The buffered operations to apply.
910
+ * @param sharedTx - The shared IDBTransaction (IndexedDB only), or null (MemoryStore).
911
+ */
912
+ executeInTransaction(ops: BufferedOperation<T>[], sharedTx: IDBTransaction | null): Promise<void>;
702
913
  }
703
914
  interface Collection<T> {
704
915
  /**
705
916
  * Finds a single document matching the query.
706
- * @param query - The query to execute.
707
- * @returns A promise resolving to the matching document or `null` if not found.
708
917
  */
709
918
  find: (query: QueryFilter<T>) => Promise<Document<T> | null>;
710
919
  /**
711
- * Lists documents based on the provided query.
712
- * @param query - The query to list documents (supports pagination and sorting).
713
- * @returns A promise resolving to an array of documents.
920
+ * Lists documents with pagination. Returns an AsyncIterator so consumers can
921
+ * wrap it in their own iteration protocol (e.g. for-await-of via AsyncIterable).
714
922
  */
715
923
  list: (query: PaginationOptions) => Promise<AsyncIterator<Document<T>[]>>;
716
924
  /**
717
- * Filters documents based on the provided query.
718
- * @param query - The query to filter documents.
719
- * @returns A promise resolving to an array of matching documents.
925
+ * Filters all documents matching the query.
720
926
  */
721
927
  filter: (query: QueryFilter<T>) => Promise<Document<T>[]>;
722
928
  /**
723
- * Creates a new document in the schema.
929
+ * Creates a new document in the collection.
930
+ *
931
+ * When a TransactionContext is provided the initial store.add is buffered into
932
+ * the transaction rather than written immediately. The document is returned in
933
+ * its fully initialised in-memory state regardless — callers can use it before
934
+ * the transaction commits.
935
+ *
724
936
  * @param initial - The initial data for the document.
725
- * @returns A promise resolving to the created document.
937
+ * @param tx - Optional transaction to buffer the write into.
726
938
  */
727
- create: (initial: T) => Promise<Document<T>>;
939
+ create: (initial: T, tx?: TransactionContext) => Promise<Document<T>>;
728
940
  /**
729
- * Subscribes to schema-level events (e.g., "create", "update", "delete", "access").
730
- * @param event - The event type to subscribe to.
731
- * @param callback - The function to call when the event occurs.
732
- * @returns A promise resolving to an unsubscribe function.
941
+ * Subscribes to collection-level events.
733
942
  */
734
- subscribe: (event: CollectionEventType | TelemetryEventType, callback: (event: CollectionEvent<T> | TelemetryEvent) => void) => Promise<() => void>;
943
+ subscribe: (event: CollectionEventType | TelemetryEventType, callback: (event: CollectionEvent<T> | TelemetryEvent) => void) => () => void;
735
944
  /**
736
- * Validate data
737
- * @param data - The data to validate
738
- * @returns An object containing validation results
945
+ * Validates data against the collection's schema.
739
946
  */
740
947
  validate(data: Record<string, any>): Promise<{
741
948
  value?: any;
@@ -744,9 +951,10 @@ interface Collection<T> {
744
951
  path: Array<string>;
745
952
  }>;
746
953
  }>;
954
+ invalidate(): void;
747
955
  }
748
956
  /**
749
- * Event payload for DocumentCursor events.
957
+ * Event payload for Collection events.
750
958
  */
751
959
  type CollectionEventType = "document:create" | "collection:read" | "migration:start" | "migration:end";
752
960
  type CollectionEvent<T> = {
@@ -759,91 +967,49 @@ type CollectionEvent<T> = {
759
967
  };
760
968
  interface Database {
761
969
  /**
762
- * Accesses a schema model by name.
763
- * @param schemaName - The name of the schema to access.
764
- * @returns A promise resolving to the schema's DocumentCursor.
765
- * @throws DatabaseError
970
+ * Opens an existing collection by name.
766
971
  */
767
972
  collection: <T>(schemaName: string) => Promise<Collection<T>>;
768
973
  /**
769
- * Creates a new schema model.
770
- * @param schema - The schema definition.
771
- * @returns A promise resolving to the created schema's DocumentCursor.
772
- * @throws DatabaseError
974
+ * Creates a new collection from a schema definition.
773
975
  */
774
976
  createCollection: <T>(schema: SchemaDefinition) => Promise<Collection<T>>;
775
977
  /**
776
- * Deletes a schema by name.
777
- * @param schemaName - The name of the schema to delete.
778
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
779
- * @throws DatabaseError
978
+ * Deletes a collection and its schema record.
780
979
  */
781
980
  deleteCollection: (schemaName: string) => Promise<boolean>;
782
981
  /**
783
- * Updates an existing schema.
784
- * @param schema - The updated schema definition.
785
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
786
- * @throws DatabaseError
982
+ * Updates an existing collection's schema record.
787
983
  */
788
984
  updateCollection: (schema: SchemaDefinition) => Promise<boolean>;
789
985
  /**
790
- * Migrates an existing collection's data and updates its schema definition metadata.
791
- * This function processes data in a streaming fashion to prevent loading
792
- * the entire collection into memory.
793
- *
794
- * It will:
795
- * 1. Verify the target collection exists.
796
- * 2. Retrieve the collection's current schema definition from a metadata store ($index).
797
- * 3. Initialize a `MigrationEngine` with this schema.
798
- * 4. Allow a callback to define specific data transformations using the `MigrationEngine`.
799
- * 5. **Crucially, it uses `migrationEngine.dryRun()` to get the `newSchema` that results**
800
- * **from the transformations defined in the callback.**
801
- * 6. Execute these transformations by streaming data from the collection,
802
- * through the `MigrationEngine`, and back into the same collection.
803
- * 7. Finally, update the schema definition for the collection in the `$index` metadata store
804
- * to reflect this `newSchema`.
805
- * All these steps for data and metadata updates happen within a single atomic IndexedDB transaction.
806
- *
807
- * Note: This function focuses solely on *data transformation* and *metadata updates*.
808
- * It does NOT handle structural IndexedDB changes like adding/removing physical indexes or object stores,
809
- * which still require an `onupgradeneeded` event (i.e., a database version upgrade).
810
- *
811
- * @param name - The name of the collection (IndexedDB object store) to migrate.
812
- * @param {Object} opts - Options for the new migration
813
- * @param {SchemaChange<any>[]} opts.changes - Array of schema changes
814
- * @param {string} opts.description - Description of the migration
815
- * @param {SchemaChange<any>[]} [opts.rollback] - Optional rollback changes
816
- * @param {DataTransform<any, any>} [opts.transform] - Optional data transform
817
- * @returns A Promise resolving to `true` if the migration completes successfully,
818
- * @throws {DatabaseError} If the collection does not exist, its schema metadata is missing,
819
- * or any IndexedDB operation/streaming fails critically.
986
+ * Migrates an existing collection's data and schema definition.
987
+ * Processes data in a streaming fashion to avoid loading the full collection
988
+ * into memory.
820
989
  */
821
- migrateCollection: (name: string, opts: CollectionMigrationOptions, batchSize?: number) => Promise<boolean>;
990
+ migrateCollection: <T>(name: string, opts: CollectionMigrationOptions, batchSize?: number) => Promise<Collection<T>>;
822
991
  /**
823
- * Subscribes to database-level events (e.g., "schemaAdded", "schemaDeleted", "schemaAccessed", "migrate").
824
- * @param event - The event type to subscribe to.
825
- * @param callback - The function to call when the event occurs.
826
- * @returns A promise resolving to an unsubscribe function.
992
+ * Executes a callback within a TransactionContext.
993
+ * Writes buffered inside the callback are flushed atomically on commit.
994
+ * If the callback throws, the buffer is discarded (no writes are flushed).
827
995
  */
828
- subscribe: (event: DatabaseEventType | "telemetry", callback: (event: DatabaseEvent | TelemetryEvent) => void) => Promise<() => void>;
996
+ transaction: (callback: (tx: TransactionContext) => Promise<void>) => Promise<void>;
829
997
  /**
830
- * Closes the connection to the database
998
+ * Subscribes to database-level events.
999
+ */
1000
+ subscribe: (event: DatabaseEventType | "telemetry", callback: (event: DatabaseEvent | TelemetryEvent) => void) => () => void;
1001
+ /**
1002
+ * Releases in-memory references and event bus subscriptions.
1003
+ * Does not delete any persisted data.
831
1004
  */
832
1005
  close: () => void;
1006
+ clear: () => Promise<void>;
833
1007
  /**
834
- * Ensures a collection exists; creates it if it doesn't.
835
- * Idempotent – safe to call multiple times.
836
- * @param schema - The schema definition for the collection.
837
- * @returns A promise that resolves when the collection exists.
838
- * @throws {DatabaseError} If validation fails or an unexpected error occurs.
839
- */
1008
+ * Ensures a collection exists; creates it if it doesn't. Idempotent.
1009
+ */
840
1010
  ensureCollection: (schema: SchemaDefinition) => Promise<void>;
841
1011
  /**
842
- * Ensures multiple collections exist; creates any that don't.
843
- * Idempotent – safe to call multiple times.
844
- * @param schemas - An array of schema definitions.
845
- * @returns A promise that resolves when all collections exist.
846
- * @throws {DatabaseError} If any schema validation fails or an unexpected error occurs.
1012
+ * Ensures multiple collections exist; creates any that don't. Idempotent.
847
1013
  */
848
1014
  setupCollections: (schemas: SchemaDefinition[]) => Promise<void>;
849
1015
  }
@@ -853,9 +1019,6 @@ type CollectionMigrationOptions = {
853
1019
  rollback?: SchemaChange<any>[];
854
1020
  transform?: string | DataTransform<any, any>;
855
1021
  };
856
- /**
857
- * Event payload for Database events.
858
- */
859
1022
  type DatabaseEventType = "collection:create" | "collection:delete" | "collection:update" | "collection:read" | "migrate";
860
1023
  type DatabaseEvent = {
861
1024
  type: DatabaseEventType;
@@ -865,60 +1028,17 @@ type DatabaseEvent = {
865
1028
  type Document<T> = {
866
1029
  readonly [K in keyof T]: T[K];
867
1030
  } & {
868
- /**
869
- * Unique identifier for the document (assigned automatically if not provided).
870
- */
871
1031
  $id?: string;
872
- /**
873
- * Timestamp of document creation (ISO string or Date).
874
- */
875
1032
  $created?: string | Date;
876
- /**
877
- * Timestamp of last document update (ISO string or Date).
878
- */
879
1033
  $updated?: string | Date;
880
- /**
881
- * Version number incremented on each change (used for optimistic concurrency control).
882
- */
883
1034
  $version?: number;
884
- /**
885
- * Fetches the latest data from the database and updates the document instance.
886
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
887
- */
888
1035
  read: () => Promise<boolean>;
889
- /**
890
- * Saves the current document state to the database.
891
- * Normally called automatically by `update()` or `delete()`, but can be used manually.
892
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
893
- */
894
1036
  save: (tx?: TransactionContext) => Promise<boolean>;
895
- /**
896
- * Updates the document with the provided properties.
897
- * @param props - Partial object containing the fields to update.
898
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
899
- */
900
- update: (props: Partial<T>) => Promise<boolean>;
901
- /**
902
- * Deletes the document from the database.
903
- * @returns A promise resolving to `true` if successful, or `false` if an error occurs.
904
- */
905
- delete: () => Promise<boolean>;
906
- /**
907
- * Subscribes to document events (e.g., "update", "delete", "access").
908
- * @param event - The event type to subscribe to.
909
- * @param callback - The function to call when the event occurs.
910
- * @returns An unsubscribe function.
911
- */
1037
+ update: (props: Partial<T>, tx?: TransactionContext) => Promise<boolean>;
1038
+ delete: (tx?: TransactionContext) => Promise<boolean>;
912
1039
  subscribe: (event: DocumentEventType | TelemetryEventType, callback: (event: DocumentEvent<T> | TelemetryEvent) => void) => () => void;
913
- /**
914
- * Returns a plain object containing only the user-defined data (without system fields like $id, $version, etc.).
915
- * @returns The current user data.
916
- */
917
1040
  state(): T;
918
1041
  };
919
- /**
920
- * Event payload for DocumentModel events.
921
- */
922
1042
  type DocumentEventType = "document:create" | "document:write" | "document:update" | "document:delete" | "document:read";
923
1043
  type DocumentEvent<T> = {
924
1044
  type: DocumentEventType;
@@ -942,7 +1062,7 @@ type TelemetryEvent = {
942
1062
  document?: string;
943
1063
  };
944
1064
  result?: {
945
- type: 'array' | string;
1065
+ type: "array" | string;
946
1066
  size?: number;
947
1067
  };
948
1068
  error: {
@@ -979,25 +1099,19 @@ declare const COLLECTIONS: {
979
1099
  readonly CONTEXT: "context";
980
1100
  readonly SESSION: "session";
981
1101
  readonly TURN: "turn";
1102
+ readonly TASK: "task";
982
1103
  };
983
1104
  type CollectionName = typeof COLLECTIONS[keyof typeof COLLECTIONS];
984
1105
 
985
- /**
986
- * Utility type for representing partial updates to the state, allowing deep nesting.
987
- * It makes all properties optional and applies the same transformation recursively
988
- * to nested objects and array elements, allowing for selective updates while
989
- * preserving the original structure. It also includes the original type T and
990
- * undefined as possibilities for the top level and nested values.
991
- */
992
- type DeepPartial<T> = T extends object ? T extends readonly (infer U)[] ? readonly (DeepPartial<U> | undefined)[] | undefined | T : T extends (infer U)[] ? (DeepPartial<U> | undefined)[] | undefined | T : {
993
- [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> | undefined : T[K] | undefined;
994
- } | undefined | T : T | undefined;
995
-
996
1106
  declare function del<T>(): T;
997
1107
  declare const merge: <T extends object>(original: T, changes: DeepPartial<T> | symbol) => T;
998
1108
  declare function ok<T>(value: T): Result<T, never>;
999
1109
  declare function err<E = WorkspaceError>(error: E): Result<never, E>;
1000
1110
  declare function omitNullUndefined<T extends Record<string, any>>(obj: T): Partial<T>;
1111
+ declare function createProjectWorkspace<ProjectMetadata extends Record<string, any> = Record<string, any>>({ project, language }: {
1112
+ language: string;
1113
+ project: Omit<Project<ProjectMetadata>, "id">;
1114
+ }): Workspace<ProjectMetadata>;
1001
1115
  declare function createSimpleWorkspace({ name, owner, language }: {
1002
1116
  name: string;
1003
1117
  language: string;
@@ -1053,10 +1167,6 @@ interface BlobStorage {
1053
1167
  registerBlob?(record: BlobRecord, data: Uint8Array): Promise<void>;
1054
1168
  }
1055
1169
 
1056
- interface TurnRef {
1057
- id: UUID;
1058
- version: number;
1059
- }
1060
1170
  declare class TurnTree {
1061
1171
  private readonly db;
1062
1172
  constructor(db: WorkspaceDatabase);
@@ -1067,15 +1177,22 @@ declare class TurnTree {
1067
1177
  * Uses only declared schema fields — never $id.
1068
1178
  */
1069
1179
  private turnFilter;
1180
+ private loadRaw;
1070
1181
  getHead(sessionId: UUID): Promise<TurnRef | null>;
1071
1182
  setHead(sessionId: UUID, head: TurnRef | null): Promise<void>;
1183
+ save(sessionId: UUID, turn: Turn, head?: TurnRef | null): Promise<Turn>;
1072
1184
  append(sessionId: UUID, turn: Turn): Promise<Turn>;
1073
1185
  appendBatch(sessionId: UUID, turns: Turn[], finalHead: TurnRef | null): Promise<void>;
1074
1186
  replaceVersion(sessionId: UUID, newTurn: Turn): Promise<void>;
1075
1187
  branch(sessionId: UUID, newTurn: Turn): Promise<void>;
1076
1188
  loadAllTurns(sessionId: UUID): Promise<Turn[]>;
1077
- getActiveChain(sessionId: UUID, dirtyBuffer?: readonly Turn[]): Promise<Turn[]>;
1189
+ loadTurnVersions(sessionId: UUID, turnId: UUID): Promise<Turn[]>;
1190
+ buildNodes(turns: Turn[], head: TurnRef | null, dirtyBuffer?: readonly Turn[]): Record<UUID, TurnNode>;
1078
1191
  buildNodeGraph(sessionId: UUID, dirtyBuffer?: readonly Turn[]): Promise<Record<UUID, TurnNode>>;
1192
+ getActiveChain(sessionId: UUID, dirtyBuffer?: readonly Turn[]): Promise<Turn[]>;
1193
+ buildActiveChain(sessionId: UUID): Promise<TurnNode[]>;
1194
+ getTurnSiblings(turnId: UUID, sessionId: UUID): Promise<TurnNode[]>;
1195
+ branchInfo(turnId: UUID, sessionId: UUID): Promise<BranchInfo>;
1079
1196
  deleteSubtree(sessionId: UUID, turnId: UUID, version: number, newHead: TurnRef | null): Promise<void>;
1080
1197
  copyTranscript(sourceSessionId: UUID, targetSessionId: UUID): Promise<void>;
1081
1198
  }
@@ -1092,11 +1209,13 @@ interface BlobStoreConfig {
1092
1209
  declare class BlobStore {
1093
1210
  private readonly storage;
1094
1211
  private readonly config;
1212
+ private bus;
1095
1213
  /**
1096
1214
  * In-memory cache of BlobRecords. Kept consistent with the backend on
1097
1215
  * every write. Records are small (no bytes), so we cache all of them.
1098
1216
  */
1099
1217
  private readonly recordCache;
1218
+ constructor(backend: BlobStorage, bus: EventBus<WorkspaceEvents>, config?: BlobStoreConfig);
1100
1219
  /**
1101
1220
  * Called after any operation that changes the blob registry.
1102
1221
  * Passes the full BlobRecord (or null on deletion). The integration
@@ -1106,8 +1225,7 @@ declare class BlobStore {
1106
1225
  * that only need summary fields (sha256, mediaType, sizeBytes, etc.)
1107
1226
  * can read them directly from BlobRecord without a separate type.
1108
1227
  */
1109
- onRegistryChanged?: (sha256: SHA256, record: BlobRecord | null) => void;
1110
- constructor(backend: BlobStorage, config?: BlobStoreConfig);
1228
+ subscribe<TEventName extends keyof WorkspaceEvents>(event: TEventName, callback: (payload: WorkspaceEvents[TEventName]) => void): () => void;
1111
1229
  init(): Promise<void>;
1112
1230
  register(data: Uint8Array, mediaType: BlobMediaType, filename?: string): Promise<Result<BlobRef, WorkspaceError>>;
1113
1231
  retain(sha256: SHA256): Promise<Result<void, WorkspaceError>>;
@@ -1123,7 +1241,6 @@ declare class BlobStore {
1123
1241
  }>;
1124
1242
  }>;
1125
1243
  gc(): Promise<number>;
1126
- gcFull(): Promise<number>;
1127
1244
  purge(sha256: SHA256): Promise<Result<void, WorkspaceError>>;
1128
1245
  getRecord(sha256: SHA256): BlobRecord | null;
1129
1246
  /**
@@ -1140,17 +1257,11 @@ declare class ContentStore {
1140
1257
  private readonly roleCache;
1141
1258
  private readonly preferenceCache;
1142
1259
  private readonly contextCache;
1143
- /**
1144
- * Called after any blob registry change with the sha256 and updated
1145
- * BlobRecord (null on deletion). Wire this to your state manager to
1146
- * keep Index.blobs current.
1147
- *
1148
- * ContentStore sets this on BlobStore.onRegistryChanged internally.
1149
- * Consumers set this property to receive the forwarded notifications.
1150
- */
1151
- onBlobRegistryChanged?: (sha256: SHA256, record: BlobRecord | null) => void;
1260
+ private readonly taskCache;
1261
+ private bus;
1262
+ subscribe<TEventName extends keyof WorkspaceEvents>(event: TEventName, callback: (payload: WorkspaceEvents[TEventName]) => void): () => void;
1152
1263
  private constructor();
1153
- static create(db: WorkspaceDatabase, blobStorage: BlobStorage, config?: ContentStoreConfig): Promise<ContentStore>;
1264
+ static create(db: WorkspaceDatabase, blobStorage: BlobStorage, eventBus: EventBus<WorkspaceEvents>, config?: ContentStoreConfig): Promise<ContentStore>;
1154
1265
  private init;
1155
1266
  getTurnTree(): TurnTree;
1156
1267
  getRole(name: string): Promise<Result<Role, WorkspaceError>>;
@@ -1163,6 +1274,10 @@ declare class ContentStore {
1163
1274
  saveContext(context: Context): Promise<void>;
1164
1275
  deleteContext(key: string): Promise<void>;
1165
1276
  getContextByTopics(indexState: Index, topics: string[]): Promise<Context[]>;
1277
+ getTask(id: UUID): Promise<Result<Task, WorkspaceError>>;
1278
+ saveTask(task: Task): Promise<void>;
1279
+ deleteTask(id: UUID): Promise<void>;
1280
+ getTasksByTopics(indexState: Index, topics: string[]): Promise<Task[]>;
1166
1281
  saveSession(meta: SessionMeta): Promise<void>;
1167
1282
  updateSessionMeta(sessionId: UUID, patch: DeepPartial<SessionMeta>): Promise<void>;
1168
1283
  deleteSession(sessionId: UUID): Promise<void>;
@@ -1173,43 +1288,75 @@ declare class ContentStore {
1173
1288
  recordBlobRemoteId(sha256: SHA256, providerId: string, fileId: string): Promise<Result<void, WorkspaceError>>;
1174
1289
  getBlobRecord(sha256: SHA256): BlobRecord | null;
1175
1290
  getAllBlobRecords(): Record<SHA256, BlobRecord>;
1176
- /**
1177
- * Returns the blob resolver function expected by PromptBuilder.
1178
- * Closes over the internal BlobStore — no BlobStore reference leaks out.
1179
- */
1180
1291
  getBlobResolver(): BlobStore['resolveRefs'];
1181
- recordTurn(sessionId: UUID, turn: Turn): Promise<Result<void, WorkspaceError>>;
1292
+ appendTurn(sessionId: UUID, turn: Turn): Promise<Result<Turn, WorkspaceError>>;
1293
+ saveTurn(sessionId: UUID, turn: Turn): Promise<Result<Turn, WorkspaceError>>;
1182
1294
  editTurn(sessionId: UUID, turnId: UUID, newBlocks: ContentBlock[], newVersion: number, roleSnapshot?: string): Promise<Result<void, WorkspaceError>>;
1183
1295
  branchTurn(sessionId: UUID, newTurn: Turn): Promise<Result<void, WorkspaceError>>;
1184
1296
  deleteTurnSubtree(sessionId: UUID, turnId: UUID, version: number, newHead: TurnRef | null): Promise<Result<void, WorkspaceError>>;
1185
1297
  copyTranscript(sourceSessionId: UUID, targetSessionId: UUID): Promise<void>;
1186
1298
  resolveSession(workspace: Workspace, sessionId: UUID, transcript?: Turn[]): Promise<Result<EffectiveSession, WorkspaceError>>;
1299
+ gc(): Promise<number>;
1187
1300
  }
1188
1301
 
1189
1302
  declare function workspaceReducer({ index: state }: Workspace, command: Command): Result<DeepPartial<Workspace>, WorkspaceError>;
1190
1303
 
1304
+ interface Summarizer {
1305
+ summarize(transcript: Turn[], tokenBudget: number): Promise<{
1306
+ summary: string;
1307
+ remaining: Turn[];
1308
+ }>;
1309
+ }
1310
+ interface ToolRegistry {
1311
+ /**
1312
+ * Subscribe to tool registry changes.
1313
+ * The callback is executed whenever the set of available tools changes.
1314
+ */
1315
+ onRegistryChanged(callback: (tools: ToolSummary[]) => void): void;
1316
+ /**
1317
+ * Returns all tools currently available in the environment.
1318
+ */
1319
+ list(): ToolSummary[];
1320
+ /**
1321
+ * The stateless executor.
1322
+ * Takes the call, returns the raw result.
1323
+ */
1324
+ execute<T = any>(call: ToolCall): Promise<Result<T>>;
1325
+ }
1326
+ interface PermissionGuard {
1327
+ /**
1328
+ * An async predicate that determines if the current context
1329
+ * (user, session, project) has the right to execute the request.
1330
+ */
1331
+ authenticate(request: AuthRequest): Promise<Result<null>>;
1332
+ }
1333
+
1191
1334
  declare class WorkspaceManager {
1192
1335
  private readonly contentStore;
1336
+ private readonly permissionGuard?;
1337
+ private readonly toolRegistry?;
1338
+ private bus;
1193
1339
  /**
1194
- * Called when BlobStore triggers a registry change outside of a dispatch()
1195
- * call — e.g. eagerEviction deleting a blob on release. The caller should
1196
- * merge this patch into their Workspace.
1340
+ * Called after any workspace change
1197
1341
  */
1198
- onWorkspacePatch?: (patch: DeepPartial<Workspace>) => void;
1199
- constructor(contentStore: ContentStore);
1342
+ subscribe<TEventName extends keyof WorkspaceEvents>(event: TEventName, callback: (payload: WorkspaceEvents[TEventName]) => void): () => void;
1343
+ constructor(options: {
1344
+ contentStore: ContentStore;
1345
+ permissionGuard?: PermissionGuard;
1346
+ toolRegistry?: ToolRegistry;
1347
+ eventBus: EventBus<WorkspaceEvents>;
1348
+ });
1349
+ /**
1350
+ * Initializes a Workspace's in-memory Index from environment-static
1351
+ * sources (like ToolRegistry). Call this when opening a Workspace.
1352
+ */
1353
+ init(_: Workspace): DeepPartial<Workspace>;
1200
1354
  reduce(workspace: Workspace, command: Command): Result<DeepPartial<Workspace>, WorkspaceError>;
1201
1355
  dispatch(workspace: Workspace, command: Command): Promise<Result<DeepPartial<Workspace>, WorkspaceError>>;
1202
- resolveSession(workspace: Workspace, sessionId: UUID): Promise<Result<EffectiveSession, WorkspaceError>>;
1356
+ resolveSession(workspace: Workspace, sessionId: UUID, transcript?: Turn[]): Promise<Result<EffectiveSession, WorkspaceError>>;
1203
1357
  private handleContentSideEffects;
1204
1358
  }
1205
1359
 
1206
- interface Summarizer {
1207
- summarize(transcript: Turn[], tokenBudget: number): Promise<{
1208
- summary: string;
1209
- remaining: Turn[];
1210
- }>;
1211
- }
1212
-
1213
1360
  interface ContextRankingInput {
1214
1361
  entries: Context[];
1215
1362
  recentMessages: string[];
@@ -1223,12 +1370,14 @@ interface PlanningInput {
1223
1370
  persona: string;
1224
1371
  preferences: Preference[];
1225
1372
  context: Context[];
1373
+ task: Task | null;
1226
1374
  transcript: Turn[];
1227
1375
  budget?: TokenBudget;
1228
1376
  }
1229
1377
  interface PlanningOutput {
1230
1378
  preferences: Preference[];
1231
1379
  context: Context[];
1380
+ task: Task | null;
1232
1381
  transcript: Turn[];
1233
1382
  truncated: {
1234
1383
  preferences: number;
@@ -1280,49 +1429,87 @@ declare class PromptBuilder {
1280
1429
  private resolvePreferenceConflicts;
1281
1430
  }
1282
1431
 
1432
+ /**
1433
+ * A fluent builder for creating Turn objects.
1434
+ * This class helps construct a Turn object by adding various content blocks.
1435
+ */
1436
+ declare class TurnBuilder {
1437
+ private readonly _owner;
1438
+ private _turn;
1439
+ constructor(_owner: TurnRole, initialTurn?: Turn);
1440
+ addText(text: string): TurnBuilder;
1441
+ addImage(ref?: BlobRef, altText?: string): TurnBuilder;
1442
+ addDocument(ref?: BlobRef, title?: string): TurnBuilder;
1443
+ addToolUse(name: string, input: Record<string, unknown>): TurnBuilder;
1444
+ addToolResult(useId: UUID, content: string | Record<string, unknown>, isError?: boolean): TurnBuilder;
1445
+ addSummary(text: string): TurnBuilder;
1446
+ addRoleTransition(previousRole: string | null, newRole: string): TurnBuilder;
1447
+ addThinking(thinking: string): TurnBuilder;
1448
+ /**
1449
+ * Adds a task proposal block.
1450
+ * @param title The task title.
1451
+ * @param steps Array of step objects (each with text and optional id/status). Status defaults to 'todo'.
1452
+ * @param action The action type ('create', 'update', 'complete'). Defaults to 'create'.
1453
+ * @param taskId Optional existing task ID (for update/complete).
1454
+ */
1455
+ addTaskProposal(title: string, steps: {
1456
+ text: string;
1457
+ status?: 'todo' | 'done';
1458
+ id?: UUID;
1459
+ }[], action?: 'create' | 'update' | 'complete', taskId?: UUID): TurnBuilder;
1460
+ addBlock(block: ContentBlock): TurnBuilder;
1461
+ deleteBlock(blockId: UUID): TurnBuilder;
1462
+ editTextBlock(blockId: UUID, newText: string): TurnBuilder;
1463
+ withId(id: UUID): TurnBuilder;
1464
+ withVersion(version: number): TurnBuilder;
1465
+ withTimestamp(timestamp: Timestamp): TurnBuilder;
1466
+ withParent(parent: TurnRef): TurnBuilder;
1467
+ withRoleSnapshot(roleSnapshot: string): TurnBuilder;
1468
+ build(): Turn;
1469
+ }
1470
+
1283
1471
  declare class Session {
1284
- /** The session ID this object manages. */
1285
- readonly sessionId: UUID;
1472
+ private readonly sessionId;
1473
+ private readonly tree;
1474
+ private readonly manager;
1475
+ constructor(sessionId: UUID, tree: TurnTree, manager: WorkspaceManager);
1476
+ turns(): Promise<TurnNode[]>;
1477
+ siblings(turnId: UUID): Promise<TurnNode[]>;
1478
+ branchInfo(turnId: UUID): Promise<BranchInfo>;
1479
+ id(): string;
1286
1480
  /**
1287
- * The turn DAG. Keys are turn IDs. Mutated in-place on every write.
1288
- * Callers read this directly for rendering no copy is made.
1481
+ * Creates a new fluent TurnBuilder for constructing a Turn object.
1482
+ * @param owner The role of the owner of the turn (e.g., 'user', 'assistant', 'tool').
1483
+ * @returns A new TurnBuilder instance.
1289
1484
  */
1290
- readonly nodes: Record<UUID, TurnNode>;
1485
+ startTurn(owner: TurnRole): TurnBuilder;
1291
1486
  /**
1292
- * Current head of the active branch. Mirrors what is in
1293
- * workspace.index.sessions[sessionId].head after each patch is applied.
1294
- * Session keeps its own copy so reads don't require workspace.
1487
+ * Saves a constructed Turn object to the session. This method handles setting the session ID
1488
+ * and automatically determines the parent turn if not explicitly provided in the Turn object.
1489
+ * @param workspace The current workspace state.
1490
+ * @param turn The Turn object to save. It can be constructed using the TurnBuilder.
1491
+ * @param parentRef Optional TurnRef to explicitly set the parent of the turn. If not provided,
1492
+ * the current head of the active chain will be used as the parent.
1493
+ * @returns A promise resolving to a Result containing a DeepPartial<Workspace> on success,
1494
+ * or a WorkspaceError on failure.
1295
1495
  */
1296
- private head;
1297
- /** Turns not yet flushed to storage. */
1298
- private readonly dirtyBuffer;
1299
- private flushTimer;
1300
- private flushChain;
1301
- private readonly flushConfig;
1302
- private readonly tree;
1303
- private readonly contentStore;
1304
- constructor(sessionId: UUID, nodes: Record<UUID, TurnNode>, head: TurnRef | null, tree: TurnTree, contentStore: ContentStore, flushConfig?: FlushConfig);
1305
- activeChain(): TurnNode[];
1306
- siblings(turnId: UUID): TurnNode[];
1307
- branchInfo(turnId: UUID): BranchInfo;
1308
1496
  addTurn(workspace: Workspace, turn: Turn): Promise<Result<DeepPartial<Workspace>, WorkspaceError>>;
1497
+ /**
1498
+ * Updates an existing turn in the session using a fully constructed Turn object.
1499
+ * This method will dispatch a 'turn:edit' command.
1500
+ * @param workspace The current workspace state.
1501
+ * @param updatedTurn The Turn object containing the updated blocks and other properties.
1502
+ * @returns A promise resolving to a Result containing a DeepPartial<Workspace> on success,
1503
+ * or a WorkspaceError on failure.
1504
+ */
1309
1505
  editTurn(workspace: Workspace, turnId: UUID, newBlocks: ContentBlock[], roleSnapshot?: string): Promise<Result<DeepPartial<Workspace>, WorkspaceError>>;
1310
- branchFrom(workspace: Workspace, newTurn: Turn): Promise<Result<DeepPartial<Workspace>, WorkspaceError>>;
1506
+ branch(workspace: Workspace, turn: Turn): Promise<Result<DeepPartial<Workspace>, WorkspaceError>>;
1311
1507
  deleteTurn(workspace: Workspace, turnId: UUID, version: number, newHead: TurnRef | null): Promise<Result<DeepPartial<Workspace>, WorkspaceError>>;
1312
- switchVersionLeft(_: Workspace, turnId: UUID): Result<DeepPartial<Workspace>, WorkspaceError>;
1313
- switchVersionRight(_: Workspace, turnId: UUID): Result<DeepPartial<Workspace>, WorkspaceError>;
1508
+ switchVersionLeft(workspace: Workspace, turnId: UUID): Promise<Result<DeepPartial<Workspace>, WorkspaceError>>;
1509
+ switchVersionRight(workspace: Workspace, turnId: UUID): Promise<Result<DeepPartial<Workspace>, WorkspaceError>>;
1314
1510
  private switchVersion;
1315
1511
  resolve(workspace: Workspace): Promise<Result<EffectiveSession, WorkspaceError>>;
1316
- flush(): Promise<void>;
1317
- dispose(): Promise<void>;
1318
- private findSubtreeTipForVersion;
1319
- private collectSubtreeIds;
1320
- private upsertNode;
1321
- private scheduleFlush;
1322
- private cancelFlushTimer;
1323
- private doFlush;
1324
- }
1325
- declare function buildTurnNode(turn: Turn, children?: UUID[]): TurnNode;
1512
+ }
1326
1513
 
1327
1514
  interface SessionManagerConfig {
1328
1515
  flush?: Partial<FlushConfig>;
@@ -1334,11 +1521,10 @@ interface OpenResult {
1334
1521
  declare class SessionManager {
1335
1522
  private readonly workspaceManager;
1336
1523
  private readonly contentStore;
1337
- private readonly flushConfig;
1338
- constructor(workspaceManager: WorkspaceManager, contentStore: ContentStore, config?: SessionManagerConfig);
1524
+ constructor(workspaceManager: WorkspaceManager, contentStore: ContentStore);
1339
1525
  open(workspace: Workspace, sessionId: UUID): Promise<Result<OpenResult, WorkspaceError>>;
1340
- close(session: Session): Promise<void>;
1341
1526
  get workspace(): WorkspaceManager;
1527
+ close(_: Session): Promise<void>;
1342
1528
  }
1343
1529
 
1344
1530
  /**
@@ -1408,4 +1594,4 @@ declare class IndexedDBBlobStorage implements BlobStorage {
1408
1594
  registerBlob(record: BlobRecord, data: Uint8Array): Promise<void>;
1409
1595
  }
1410
1596
 
1411
- export { type AddContext, type AddPreference, type AddRole, type AddSessionTopics, type AddTurn, type BaseCommand, type BlobCommand, type BlobMediaType, type BlobRecord, type BlobRef, type BlobStorage, BlobStore, type BlobStoreConfig, type BranchInfo, type BranchTurn, COLLECTIONS, type CacheConfig, type CollectionName, type Command, type ContentBlock, ContentStore, type ContentStoreConfig, type Context, type ContextContent, type ContextRelevanceConfig, type ContextSummary, type CreateSession, type CreateWorkspace, type DeleteContext, type DeletePreference, type DeleteRole, type DeleteSession, type DeleteTurn, type DocumentBlock, type DocumentMediaType, type EditTurn, type EffectiveSession, type FlushConfig, type ForkSession, type ImageBlock, type ImageMediaType, type Index, type IndexedDBBlobConfig, IndexedDBBlobStorage, MemoryBlobStorage, type OpenResult, type OverrideSessionPreferences, type Preference, type PreferenceConflict, type PreferenceSummary, type Project, type Prompt, PromptBuilder, type PurgeBlob, type RecordBlobRemoteId, type RegisterBlob, type ReleaseBlob, type ResolvedBlob, type Result, type RetainBlob, type Role, type RoleSummary, type RoleTransitionBlock, type SHA256, Session, SessionManager, type SessionManagerConfig, type SessionMeta, type Settings, type SummaryBlock, type SwitchRole, type TextBlock, type ThinkingBlock, type Timestamp, type TokenBudget, type ToolResultBlock, type ToolUseBlock, type TopicIndex, type TranscriptWindow, type Turn, type TurnNode, type TurnRef, type TurnRole, TurnTree, type URI, type UUID, type UpdateContext, type UpdatePreference, type UpdateRole, type Workspace, type WorkspaceBundle, type WorkspaceDatabase, type WorkspaceError, WorkspaceManager, buildTurnNode, computeSHA256, createSimpleWorkspace, createWorkspaceDatabase, del, err, extractBlobRecord, extractBlobRef, merge, ok, omitNullUndefined, workspaceReducer };
1597
+ export { type AddContext, type AddPreference, type AddRole, type AddSessionTopics, type AddTask, type AddTurn, type AuthRequest, type BackendError, type BaseCommand, type BlobCommand, type BlobError, type BlobMediaType, type BlobRecord, type BlobRef, type BlobStorage, BlobStore, type BlobStoreConfig, type BranchInfo, type BranchTurn, COLLECTIONS, type CacheConfig, type CallTool, type CollectionName, type Command, type ContentBlock, ContentStore, type ContentStoreConfig, type Context, type ContextContent, type ContextRelevanceConfig, type ContextSummary, type CreateSession, type CreateWorkspace, type DeleteContext, type DeletePreference, type DeleteRole, type DeleteSession, type DeleteTask, type DeleteTurn, type DocumentBlock, type DocumentMediaType, type DuplicateKeyError, type EditTurn, type EffectiveSession, type FlushConfig, type ForkSession, type ImageBlock, type ImageMediaType, type Index, type IndexedDBBlobConfig, IndexedDBBlobStorage, type InvalidCommandError, MemoryBlobStorage, type NotFoundError, type OpenResult, type OverrideSessionPreferences, type Preference, type PreferenceConflict, type PreferenceSummary, type Project, type Prompt, PromptBuilder, type PurgeBlob, type RecordBlobRemoteId, type RegisterBlob, type ReleaseBlob, type ResolvedBlob, type Result, type RetainBlob, type Role, type RoleSummary, type RoleTransitionBlock, type SHA256, type SaveTurn, Session, SessionManager, type SessionManagerConfig, type SessionMeta, type Settings, type SummaryBlock, type SwitchRole, type Task, type TaskProposalBlock, type TaskStatus, type TaskStep, type TaskSummary, type TextBlock, type ThinkingBlock, type Timestamp, type TokenBudget, type ToolCall, type ToolResultBlock, type ToolSummary, type ToolUseBlock, type TopicIndex, type TranscriptWindow, type Turn, type TurnNode, type TurnRef, type TurnRole, TurnTree, type URI, type UUID, type UpdateContext, type UpdatePreference, type UpdateRole, type UpdateTask, type Workspace, type WorkspaceBundle, type WorkspaceDatabase, type WorkspaceError, type WorkspaceEvents, WorkspaceManager, computeSHA256, createProjectWorkspace, createSimpleWorkspace, createWorkspaceDatabase, del, err, extractBlobRecord, extractBlobRef, merge, ok, omitNullUndefined, workspaceReducer };