@weave_protocol/domere 1.0.18 → 1.2.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.
@@ -0,0 +1,894 @@
1
+ /**
2
+ * Dōmere - State Manager
3
+ *
4
+ * Distributed state management with locking, branching, and conflict resolution
5
+ * for multi-agent AI orchestration systems.
6
+ */
7
+
8
+ import * as crypto from 'crypto';
9
+
10
+ // =============================================================================
11
+ // Types
12
+ // =============================================================================
13
+
14
+ export type ConflictResolution = 'last-write-wins' | 'first-write-wins' | 'merge' | 'manual';
15
+ export type LockType = 'exclusive' | 'shared';
16
+
17
+ export interface StateEntry {
18
+ key: string;
19
+ value: any;
20
+ version: number;
21
+ hash: string;
22
+ created_at: Date;
23
+ updated_at: Date;
24
+ updated_by: string;
25
+ branch: string;
26
+ metadata: Record<string, any>;
27
+ }
28
+
29
+ export interface Lock {
30
+ id: string;
31
+ key: string;
32
+ type: LockType;
33
+ holder: string;
34
+ acquired_at: Date;
35
+ expires_at: Date;
36
+ renewed_count: number;
37
+ }
38
+
39
+ export interface LockRequest {
40
+ key: string;
41
+ holder: string;
42
+ type?: LockType;
43
+ duration_ms?: number;
44
+ wait_ms?: number; // How long to wait if locked
45
+ }
46
+
47
+ export interface LockResult {
48
+ acquired: boolean;
49
+ lock?: Lock;
50
+ reason?: string;
51
+ current_holder?: string;
52
+ retry_after_ms?: number;
53
+ }
54
+
55
+ export interface Branch {
56
+ name: string;
57
+ parent: string;
58
+ created_at: Date;
59
+ created_by: string;
60
+ head_version: number;
61
+ merged: boolean;
62
+ merged_at?: Date;
63
+ }
64
+
65
+ export interface MergeResult {
66
+ success: boolean;
67
+ conflicts: Conflict[];
68
+ merged_keys: string[];
69
+ source_branch: string;
70
+ target_branch: string;
71
+ }
72
+
73
+ export interface Conflict {
74
+ key: string;
75
+ source_value: any;
76
+ target_value: any;
77
+ source_version: number;
78
+ target_version: number;
79
+ base_value?: any;
80
+ }
81
+
82
+ export interface StateChange {
83
+ type: 'set' | 'delete' | 'merge';
84
+ key: string;
85
+ old_value?: any;
86
+ new_value?: any;
87
+ version: number;
88
+ timestamp: Date;
89
+ agent_id: string;
90
+ branch: string;
91
+ }
92
+
93
+ export interface StateSnapshot {
94
+ id: string;
95
+ branch: string;
96
+ timestamp: Date;
97
+ entries: Map<string, StateEntry>;
98
+ version: number;
99
+ }
100
+
101
+ // =============================================================================
102
+ // State Manager
103
+ // =============================================================================
104
+
105
+ export class StateManager {
106
+ private state: Map<string, Map<string, StateEntry>> = new Map(); // branch -> key -> entry
107
+ private locks: Map<string, Lock> = new Map(); // key -> lock
108
+ private branches: Map<string, Branch> = new Map();
109
+ private snapshots: Map<string, StateSnapshot> = new Map();
110
+ private changeLog: StateChange[] = [];
111
+ private changeCallbacks: ((change: StateChange) => void)[] = [];
112
+
113
+ private conflictResolution: ConflictResolution;
114
+ private defaultLockDuration: number;
115
+
116
+ constructor(options?: {
117
+ conflict_resolution?: ConflictResolution;
118
+ default_lock_duration_ms?: number;
119
+ }) {
120
+ this.conflictResolution = options?.conflict_resolution || 'last-write-wins';
121
+ this.defaultLockDuration = options?.default_lock_duration_ms || 30000;
122
+
123
+ // Initialize main branch
124
+ this.branches.set('main', {
125
+ name: 'main',
126
+ parent: '',
127
+ created_at: new Date(),
128
+ created_by: 'system',
129
+ head_version: 0,
130
+ merged: false,
131
+ });
132
+ this.state.set('main', new Map());
133
+
134
+ // Start lock cleanup timer
135
+ setInterval(() => this.cleanupExpiredLocks(), 5000);
136
+ }
137
+
138
+ // ===========================================================================
139
+ // Basic State Operations
140
+ // ===========================================================================
141
+
142
+ /**
143
+ * Get a value
144
+ */
145
+ async get(key: string, options?: { branch?: string }): Promise<any | undefined> {
146
+ const branch = options?.branch || 'main';
147
+ const branchState = this.state.get(branch);
148
+ if (!branchState) return undefined;
149
+
150
+ const entry = branchState.get(key);
151
+ return entry?.value;
152
+ }
153
+
154
+ /**
155
+ * Get entry with metadata
156
+ */
157
+ async getEntry(key: string, options?: { branch?: string }): Promise<StateEntry | undefined> {
158
+ const branch = options?.branch || 'main';
159
+ const branchState = this.state.get(branch);
160
+ if (!branchState) return undefined;
161
+
162
+ return branchState.get(key);
163
+ }
164
+
165
+ /**
166
+ * Set a value
167
+ */
168
+ async set(key: string, value: any, options?: {
169
+ branch?: string;
170
+ agent_id?: string;
171
+ metadata?: Record<string, any>;
172
+ require_lock?: boolean;
173
+ }): Promise<StateEntry> {
174
+ const branch = options?.branch || 'main';
175
+ const agentId = options?.agent_id || 'unknown';
176
+
177
+ // Check lock
178
+ if (options?.require_lock) {
179
+ const lock = this.locks.get(key);
180
+ if (!lock || lock.holder !== agentId) {
181
+ throw new Error(`Agent ${agentId} does not hold lock on ${key}`);
182
+ }
183
+ }
184
+
185
+ // Check for existing exclusive lock by another holder
186
+ const existingLock = this.locks.get(key);
187
+ if (existingLock && existingLock.type === 'exclusive' && existingLock.holder !== agentId) {
188
+ throw new Error(`Key ${key} is exclusively locked by ${existingLock.holder}`);
189
+ }
190
+
191
+ let branchState = this.state.get(branch);
192
+ if (!branchState) {
193
+ throw new Error(`Branch ${branch} does not exist`);
194
+ }
195
+
196
+ const existing = branchState.get(key);
197
+ const now = new Date();
198
+ const version = existing ? existing.version + 1 : 1;
199
+ const hash = crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
200
+
201
+ const entry: StateEntry = {
202
+ key,
203
+ value,
204
+ version,
205
+ hash,
206
+ created_at: existing?.created_at || now,
207
+ updated_at: now,
208
+ updated_by: agentId,
209
+ branch,
210
+ metadata: options?.metadata || existing?.metadata || {},
211
+ };
212
+
213
+ branchState.set(key, entry);
214
+
215
+ // Update branch head
216
+ const branchInfo = this.branches.get(branch)!;
217
+ branchInfo.head_version = Math.max(branchInfo.head_version, version);
218
+
219
+ // Log change
220
+ const change: StateChange = {
221
+ type: 'set',
222
+ key,
223
+ old_value: existing?.value,
224
+ new_value: value,
225
+ version,
226
+ timestamp: now,
227
+ agent_id: agentId,
228
+ branch,
229
+ };
230
+ this.changeLog.push(change);
231
+ this.notifyChange(change);
232
+
233
+ return entry;
234
+ }
235
+
236
+ /**
237
+ * Delete a value
238
+ */
239
+ async delete(key: string, options?: {
240
+ branch?: string;
241
+ agent_id?: string;
242
+ }): Promise<boolean> {
243
+ const branch = options?.branch || 'main';
244
+ const agentId = options?.agent_id || 'unknown';
245
+
246
+ const branchState = this.state.get(branch);
247
+ if (!branchState) return false;
248
+
249
+ const existing = branchState.get(key);
250
+ if (!existing) return false;
251
+
252
+ // Check for lock
253
+ const lock = this.locks.get(key);
254
+ if (lock && lock.type === 'exclusive' && lock.holder !== agentId) {
255
+ throw new Error(`Key ${key} is exclusively locked by ${lock.holder}`);
256
+ }
257
+
258
+ branchState.delete(key);
259
+
260
+ // Log change
261
+ const change: StateChange = {
262
+ type: 'delete',
263
+ key,
264
+ old_value: existing.value,
265
+ version: existing.version,
266
+ timestamp: new Date(),
267
+ agent_id: agentId,
268
+ branch,
269
+ };
270
+ this.changeLog.push(change);
271
+ this.notifyChange(change);
272
+
273
+ return true;
274
+ }
275
+
276
+ /**
277
+ * List all keys
278
+ */
279
+ async keys(options?: { branch?: string; prefix?: string }): Promise<string[]> {
280
+ const branch = options?.branch || 'main';
281
+ const branchState = this.state.get(branch);
282
+ if (!branchState) return [];
283
+
284
+ let keys = Array.from(branchState.keys());
285
+
286
+ if (options?.prefix) {
287
+ keys = keys.filter(k => k.startsWith(options.prefix!));
288
+ }
289
+
290
+ return keys;
291
+ }
292
+
293
+ /**
294
+ * Check if key exists
295
+ */
296
+ async has(key: string, options?: { branch?: string }): Promise<boolean> {
297
+ const branch = options?.branch || 'main';
298
+ const branchState = this.state.get(branch);
299
+ return branchState?.has(key) || false;
300
+ }
301
+
302
+ // ===========================================================================
303
+ // Locking
304
+ // ===========================================================================
305
+
306
+ /**
307
+ * Acquire a lock
308
+ */
309
+ async acquireLock(request: LockRequest): Promise<LockResult> {
310
+ const { key, holder, type = 'exclusive', duration_ms = this.defaultLockDuration, wait_ms = 0 } = request;
311
+
312
+ const existingLock = this.locks.get(key);
313
+
314
+ // Check if already locked
315
+ if (existingLock) {
316
+ // Check if expired
317
+ if (new Date() > existingLock.expires_at) {
318
+ this.locks.delete(key);
319
+ } else {
320
+ // Locked by someone else
321
+ if (existingLock.holder !== holder) {
322
+ // Can acquire shared lock if existing is shared
323
+ if (type === 'shared' && existingLock.type === 'shared') {
324
+ // Allow multiple shared locks (simplified: just extend)
325
+ } else {
326
+ // Wait or fail
327
+ if (wait_ms > 0) {
328
+ return {
329
+ acquired: false,
330
+ reason: 'Key is locked',
331
+ current_holder: existingLock.holder,
332
+ retry_after_ms: Math.min(wait_ms, existingLock.expires_at.getTime() - Date.now()),
333
+ };
334
+ }
335
+ return {
336
+ acquired: false,
337
+ reason: 'Key is locked',
338
+ current_holder: existingLock.holder,
339
+ };
340
+ }
341
+ } else {
342
+ // Same holder - renew
343
+ existingLock.expires_at = new Date(Date.now() + duration_ms);
344
+ existingLock.renewed_count++;
345
+ return { acquired: true, lock: existingLock };
346
+ }
347
+ }
348
+ }
349
+
350
+ // Create new lock
351
+ const lock: Lock = {
352
+ id: `lock_${crypto.randomUUID()}`,
353
+ key,
354
+ type,
355
+ holder,
356
+ acquired_at: new Date(),
357
+ expires_at: new Date(Date.now() + duration_ms),
358
+ renewed_count: 0,
359
+ };
360
+
361
+ this.locks.set(key, lock);
362
+
363
+ return { acquired: true, lock };
364
+ }
365
+
366
+ /**
367
+ * Release a lock
368
+ */
369
+ async releaseLock(key: string, holder: string): Promise<boolean> {
370
+ const lock = this.locks.get(key);
371
+
372
+ if (!lock) return false;
373
+ if (lock.holder !== holder) {
374
+ throw new Error(`Lock on ${key} is held by ${lock.holder}, not ${holder}`);
375
+ }
376
+
377
+ this.locks.delete(key);
378
+ return true;
379
+ }
380
+
381
+ /**
382
+ * Renew a lock
383
+ */
384
+ async renewLock(key: string, holder: string, duration_ms?: number): Promise<LockResult> {
385
+ const lock = this.locks.get(key);
386
+
387
+ if (!lock) {
388
+ return { acquired: false, reason: 'Lock not found' };
389
+ }
390
+
391
+ if (lock.holder !== holder) {
392
+ return { acquired: false, reason: 'Lock held by another holder', current_holder: lock.holder };
393
+ }
394
+
395
+ lock.expires_at = new Date(Date.now() + (duration_ms || this.defaultLockDuration));
396
+ lock.renewed_count++;
397
+
398
+ return { acquired: true, lock };
399
+ }
400
+
401
+ /**
402
+ * Check if key is locked
403
+ */
404
+ isLocked(key: string): { locked: boolean; holder?: string; expires_at?: Date } {
405
+ const lock = this.locks.get(key);
406
+
407
+ if (!lock) {
408
+ return { locked: false };
409
+ }
410
+
411
+ if (new Date() > lock.expires_at) {
412
+ this.locks.delete(key);
413
+ return { locked: false };
414
+ }
415
+
416
+ return { locked: true, holder: lock.holder, expires_at: lock.expires_at };
417
+ }
418
+
419
+ /**
420
+ * Get all locks held by an agent
421
+ */
422
+ getLocksForHolder(holder: string): Lock[] {
423
+ return Array.from(this.locks.values()).filter(l => l.holder === holder);
424
+ }
425
+
426
+ /**
427
+ * Release all locks held by an agent
428
+ */
429
+ async releaseAllLocks(holder: string): Promise<number> {
430
+ let released = 0;
431
+
432
+ for (const [key, lock] of this.locks) {
433
+ if (lock.holder === holder) {
434
+ this.locks.delete(key);
435
+ released++;
436
+ }
437
+ }
438
+
439
+ return released;
440
+ }
441
+
442
+ // ===========================================================================
443
+ // Branching
444
+ // ===========================================================================
445
+
446
+ /**
447
+ * Create a branch
448
+ */
449
+ async createBranch(name: string, options?: {
450
+ parent?: string;
451
+ created_by?: string;
452
+ }): Promise<Branch> {
453
+ if (this.branches.has(name)) {
454
+ throw new Error(`Branch ${name} already exists`);
455
+ }
456
+
457
+ const parent = options?.parent || 'main';
458
+ const parentBranch = this.branches.get(parent);
459
+ if (!parentBranch) {
460
+ throw new Error(`Parent branch ${parent} does not exist`);
461
+ }
462
+
463
+ const parentState = this.state.get(parent)!;
464
+
465
+ // Create branch info
466
+ const branch: Branch = {
467
+ name,
468
+ parent,
469
+ created_at: new Date(),
470
+ created_by: options?.created_by || 'unknown',
471
+ head_version: parentBranch.head_version,
472
+ merged: false,
473
+ };
474
+
475
+ this.branches.set(name, branch);
476
+
477
+ // Copy state from parent
478
+ const branchState = new Map<string, StateEntry>();
479
+ for (const [key, entry] of parentState) {
480
+ branchState.set(key, { ...entry, branch: name });
481
+ }
482
+ this.state.set(name, branchState);
483
+
484
+ return branch;
485
+ }
486
+
487
+ /**
488
+ * List branches
489
+ */
490
+ listBranches(): Branch[] {
491
+ return Array.from(this.branches.values());
492
+ }
493
+
494
+ /**
495
+ * Get branch info
496
+ */
497
+ getBranch(name: string): Branch | undefined {
498
+ return this.branches.get(name);
499
+ }
500
+
501
+ /**
502
+ * Merge branch into target
503
+ */
504
+ async merge(source: string, target: string, options?: {
505
+ agent_id?: string;
506
+ resolution?: ConflictResolution;
507
+ }): Promise<MergeResult> {
508
+ const sourceBranch = this.branches.get(source);
509
+ const targetBranch = this.branches.get(target);
510
+
511
+ if (!sourceBranch) throw new Error(`Source branch ${source} does not exist`);
512
+ if (!targetBranch) throw new Error(`Target branch ${target} does not exist`);
513
+ if (sourceBranch.merged) throw new Error(`Branch ${source} already merged`);
514
+
515
+ const sourceState = this.state.get(source)!;
516
+ const targetState = this.state.get(target)!;
517
+
518
+ const conflicts: Conflict[] = [];
519
+ const mergedKeys: string[] = [];
520
+ const resolution = options?.resolution || this.conflictResolution;
521
+
522
+ // Find all keys
523
+ const allKeys = new Set([...sourceState.keys(), ...targetState.keys()]);
524
+
525
+ for (const key of allKeys) {
526
+ const sourceEntry = sourceState.get(key);
527
+ const targetEntry = targetState.get(key);
528
+
529
+ // Key only in source - add to target
530
+ if (sourceEntry && !targetEntry) {
531
+ targetState.set(key, { ...sourceEntry, branch: target });
532
+ mergedKeys.push(key);
533
+ continue;
534
+ }
535
+
536
+ // Key only in target - keep
537
+ if (!sourceEntry && targetEntry) {
538
+ continue;
539
+ }
540
+
541
+ // Both have key - check for conflict
542
+ if (sourceEntry && targetEntry) {
543
+ if (sourceEntry.hash === targetEntry.hash) {
544
+ // Same value, no conflict
545
+ continue;
546
+ }
547
+
548
+ // Conflict!
549
+ const conflict: Conflict = {
550
+ key,
551
+ source_value: sourceEntry.value,
552
+ target_value: targetEntry.value,
553
+ source_version: sourceEntry.version,
554
+ target_version: targetEntry.version,
555
+ };
556
+
557
+ // Apply resolution strategy
558
+ if (resolution === 'last-write-wins') {
559
+ if (sourceEntry.updated_at > targetEntry.updated_at) {
560
+ targetState.set(key, { ...sourceEntry, branch: target, version: targetEntry.version + 1 });
561
+ mergedKeys.push(key);
562
+ }
563
+ // else keep target
564
+ } else if (resolution === 'first-write-wins') {
565
+ if (sourceEntry.updated_at < targetEntry.updated_at) {
566
+ targetState.set(key, { ...sourceEntry, branch: target, version: targetEntry.version + 1 });
567
+ mergedKeys.push(key);
568
+ }
569
+ // else keep target
570
+ } else if (resolution === 'merge') {
571
+ // Try to merge objects
572
+ if (typeof sourceEntry.value === 'object' && typeof targetEntry.value === 'object') {
573
+ const merged = { ...targetEntry.value, ...sourceEntry.value };
574
+ targetState.set(key, {
575
+ ...targetEntry,
576
+ value: merged,
577
+ version: targetEntry.version + 1,
578
+ updated_at: new Date(),
579
+ hash: crypto.createHash('sha256').update(JSON.stringify(merged)).digest('hex'),
580
+ });
581
+ mergedKeys.push(key);
582
+ } else {
583
+ conflicts.push(conflict);
584
+ }
585
+ } else {
586
+ // Manual resolution needed
587
+ conflicts.push(conflict);
588
+ }
589
+ }
590
+ }
591
+
592
+ // Mark source as merged if no conflicts
593
+ if (conflicts.length === 0) {
594
+ sourceBranch.merged = true;
595
+ sourceBranch.merged_at = new Date();
596
+ }
597
+
598
+ return {
599
+ success: conflicts.length === 0,
600
+ conflicts,
601
+ merged_keys: mergedKeys,
602
+ source_branch: source,
603
+ target_branch: target,
604
+ };
605
+ }
606
+
607
+ /**
608
+ * Resolve conflicts manually
609
+ */
610
+ async resolveConflicts(conflicts: Conflict[], resolutions: Map<string, 'source' | 'target' | any>, options?: {
611
+ source: string;
612
+ target: string;
613
+ agent_id?: string;
614
+ }): Promise<void> {
615
+ if (!options?.source || !options?.target) {
616
+ throw new Error('Source and target branches required');
617
+ }
618
+
619
+ const sourceState = this.state.get(options.source);
620
+ const targetState = this.state.get(options.target);
621
+
622
+ if (!sourceState || !targetState) {
623
+ throw new Error('Invalid branches');
624
+ }
625
+
626
+ for (const conflict of conflicts) {
627
+ const resolution = resolutions.get(conflict.key);
628
+ if (!resolution) continue;
629
+
630
+ const targetEntry = targetState.get(conflict.key);
631
+ const sourceEntry = sourceState.get(conflict.key);
632
+
633
+ if (!targetEntry) continue;
634
+
635
+ let newValue: any;
636
+ if (resolution === 'source' && sourceEntry) {
637
+ newValue = sourceEntry.value;
638
+ } else if (resolution === 'target') {
639
+ continue; // Keep target
640
+ } else {
641
+ newValue = resolution; // Custom value
642
+ }
643
+
644
+ targetState.set(conflict.key, {
645
+ ...targetEntry,
646
+ value: newValue,
647
+ version: targetEntry.version + 1,
648
+ updated_at: new Date(),
649
+ updated_by: options.agent_id || 'unknown',
650
+ hash: crypto.createHash('sha256').update(JSON.stringify(newValue)).digest('hex'),
651
+ });
652
+ }
653
+
654
+ // Mark source as merged
655
+ const sourceBranch = this.branches.get(options.source);
656
+ if (sourceBranch) {
657
+ sourceBranch.merged = true;
658
+ sourceBranch.merged_at = new Date();
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Delete a branch
664
+ */
665
+ async deleteBranch(name: string): Promise<boolean> {
666
+ if (name === 'main') {
667
+ throw new Error('Cannot delete main branch');
668
+ }
669
+
670
+ const branch = this.branches.get(name);
671
+ if (!branch) return false;
672
+
673
+ this.branches.delete(name);
674
+ this.state.delete(name);
675
+
676
+ return true;
677
+ }
678
+
679
+ // ===========================================================================
680
+ // Snapshots
681
+ // ===========================================================================
682
+
683
+ /**
684
+ * Create a snapshot
685
+ */
686
+ async createSnapshot(options?: { branch?: string }): Promise<StateSnapshot> {
687
+ const branch = options?.branch || 'main';
688
+ const branchState = this.state.get(branch);
689
+ const branchInfo = this.branches.get(branch);
690
+
691
+ if (!branchState || !branchInfo) {
692
+ throw new Error(`Branch ${branch} does not exist`);
693
+ }
694
+
695
+ const snapshot: StateSnapshot = {
696
+ id: `snap_${crypto.randomUUID()}`,
697
+ branch,
698
+ timestamp: new Date(),
699
+ entries: new Map(branchState),
700
+ version: branchInfo.head_version,
701
+ };
702
+
703
+ this.snapshots.set(snapshot.id, snapshot);
704
+
705
+ return snapshot;
706
+ }
707
+
708
+ /**
709
+ * Restore from snapshot
710
+ */
711
+ async restoreSnapshot(snapshotId: string): Promise<void> {
712
+ const snapshot = this.snapshots.get(snapshotId);
713
+ if (!snapshot) {
714
+ throw new Error(`Snapshot ${snapshotId} not found`);
715
+ }
716
+
717
+ // Replace branch state
718
+ this.state.set(snapshot.branch, new Map(snapshot.entries));
719
+
720
+ // Update branch version
721
+ const branch = this.branches.get(snapshot.branch);
722
+ if (branch) {
723
+ branch.head_version = snapshot.version;
724
+ }
725
+ }
726
+
727
+ /**
728
+ * List snapshots
729
+ */
730
+ listSnapshots(branch?: string): StateSnapshot[] {
731
+ const snapshots = Array.from(this.snapshots.values());
732
+
733
+ if (branch) {
734
+ return snapshots.filter(s => s.branch === branch);
735
+ }
736
+
737
+ return snapshots;
738
+ }
739
+
740
+ /**
741
+ * Delete a snapshot
742
+ */
743
+ deleteSnapshot(snapshotId: string): boolean {
744
+ return this.snapshots.delete(snapshotId);
745
+ }
746
+
747
+ // ===========================================================================
748
+ // Change Tracking
749
+ // ===========================================================================
750
+
751
+ /**
752
+ * Get change history
753
+ */
754
+ getChanges(options?: {
755
+ branch?: string;
756
+ key?: string;
757
+ agent_id?: string;
758
+ since?: Date;
759
+ limit?: number;
760
+ }): StateChange[] {
761
+ let changes = [...this.changeLog];
762
+
763
+ if (options?.branch) {
764
+ changes = changes.filter(c => c.branch === options.branch);
765
+ }
766
+ if (options?.key) {
767
+ changes = changes.filter(c => c.key === options.key);
768
+ }
769
+ if (options?.agent_id) {
770
+ changes = changes.filter(c => c.agent_id === options.agent_id);
771
+ }
772
+ if (options?.since) {
773
+ changes = changes.filter(c => c.timestamp >= options.since!);
774
+ }
775
+
776
+ // Sort by timestamp descending
777
+ changes.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
778
+
779
+ if (options?.limit) {
780
+ changes = changes.slice(0, options.limit);
781
+ }
782
+
783
+ return changes;
784
+ }
785
+
786
+ /**
787
+ * Subscribe to changes
788
+ */
789
+ onChange(callback: (change: StateChange) => void): () => void {
790
+ this.changeCallbacks.push(callback);
791
+
792
+ return () => {
793
+ const index = this.changeCallbacks.indexOf(callback);
794
+ if (index !== -1) this.changeCallbacks.splice(index, 1);
795
+ };
796
+ }
797
+
798
+ private notifyChange(change: StateChange): void {
799
+ for (const cb of this.changeCallbacks) {
800
+ try {
801
+ cb(change);
802
+ } catch (e) {
803
+ // Ignore
804
+ }
805
+ }
806
+ }
807
+
808
+ // ===========================================================================
809
+ // Utilities
810
+ // ===========================================================================
811
+
812
+ private cleanupExpiredLocks(): void {
813
+ const now = new Date();
814
+
815
+ for (const [key, lock] of this.locks) {
816
+ if (now > lock.expires_at) {
817
+ this.locks.delete(key);
818
+ }
819
+ }
820
+ }
821
+
822
+ /**
823
+ * Get state statistics
824
+ */
825
+ getStats(): {
826
+ branches: number;
827
+ total_keys: number;
828
+ active_locks: number;
829
+ snapshots: number;
830
+ changes_logged: number;
831
+ } {
832
+ let totalKeys = 0;
833
+ for (const branchState of this.state.values()) {
834
+ totalKeys += branchState.size;
835
+ }
836
+
837
+ return {
838
+ branches: this.branches.size,
839
+ total_keys: totalKeys,
840
+ active_locks: this.locks.size,
841
+ snapshots: this.snapshots.size,
842
+ changes_logged: this.changeLog.length,
843
+ };
844
+ }
845
+
846
+ /**
847
+ * Export state for backup
848
+ */
849
+ async exportState(branch?: string): Promise<string> {
850
+ const exportData: any = {
851
+ exported_at: new Date(),
852
+ branches: branch ? [branch] : Array.from(this.branches.keys()),
853
+ data: {},
854
+ };
855
+
856
+ for (const branchName of exportData.branches) {
857
+ const branchState = this.state.get(branchName);
858
+ if (branchState) {
859
+ exportData.data[branchName] = Object.fromEntries(branchState);
860
+ }
861
+ }
862
+
863
+ return JSON.stringify(exportData, null, 2);
864
+ }
865
+
866
+ /**
867
+ * Import state from backup
868
+ */
869
+ async importState(data: string, options?: { merge?: boolean }): Promise<{ imported_keys: number }> {
870
+ const importData = JSON.parse(data);
871
+ let importedKeys = 0;
872
+
873
+ for (const branchName of Object.keys(importData.data)) {
874
+ if (!this.branches.has(branchName) && branchName !== 'main') {
875
+ await this.createBranch(branchName);
876
+ }
877
+
878
+ const branchState = this.state.get(branchName)!;
879
+ const entries = importData.data[branchName];
880
+
881
+ for (const [key, entry] of Object.entries(entries)) {
882
+ if (options?.merge && branchState.has(key)) {
883
+ continue; // Skip existing
884
+ }
885
+ branchState.set(key, entry as StateEntry);
886
+ importedKeys++;
887
+ }
888
+ }
889
+
890
+ return { imported_keys: importedKeys };
891
+ }
892
+ }
893
+
894
+ export default StateManager;