@weave_protocol/domere 1.0.18 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,665 @@
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
+ import * as crypto from 'crypto';
8
+ // =============================================================================
9
+ // State Manager
10
+ // =============================================================================
11
+ export class StateManager {
12
+ state = new Map(); // branch -> key -> entry
13
+ locks = new Map(); // key -> lock
14
+ branches = new Map();
15
+ snapshots = new Map();
16
+ changeLog = [];
17
+ changeCallbacks = [];
18
+ conflictResolution;
19
+ defaultLockDuration;
20
+ constructor(options) {
21
+ this.conflictResolution = options?.conflict_resolution || 'last-write-wins';
22
+ this.defaultLockDuration = options?.default_lock_duration_ms || 30000;
23
+ // Initialize main branch
24
+ this.branches.set('main', {
25
+ name: 'main',
26
+ parent: '',
27
+ created_at: new Date(),
28
+ created_by: 'system',
29
+ head_version: 0,
30
+ merged: false,
31
+ });
32
+ this.state.set('main', new Map());
33
+ // Start lock cleanup timer
34
+ setInterval(() => this.cleanupExpiredLocks(), 5000);
35
+ }
36
+ // ===========================================================================
37
+ // Basic State Operations
38
+ // ===========================================================================
39
+ /**
40
+ * Get a value
41
+ */
42
+ async get(key, options) {
43
+ const branch = options?.branch || 'main';
44
+ const branchState = this.state.get(branch);
45
+ if (!branchState)
46
+ return undefined;
47
+ const entry = branchState.get(key);
48
+ return entry?.value;
49
+ }
50
+ /**
51
+ * Get entry with metadata
52
+ */
53
+ async getEntry(key, options) {
54
+ const branch = options?.branch || 'main';
55
+ const branchState = this.state.get(branch);
56
+ if (!branchState)
57
+ return undefined;
58
+ return branchState.get(key);
59
+ }
60
+ /**
61
+ * Set a value
62
+ */
63
+ async set(key, value, options) {
64
+ const branch = options?.branch || 'main';
65
+ const agentId = options?.agent_id || 'unknown';
66
+ // Check lock
67
+ if (options?.require_lock) {
68
+ const lock = this.locks.get(key);
69
+ if (!lock || lock.holder !== agentId) {
70
+ throw new Error(`Agent ${agentId} does not hold lock on ${key}`);
71
+ }
72
+ }
73
+ // Check for existing exclusive lock by another holder
74
+ const existingLock = this.locks.get(key);
75
+ if (existingLock && existingLock.type === 'exclusive' && existingLock.holder !== agentId) {
76
+ throw new Error(`Key ${key} is exclusively locked by ${existingLock.holder}`);
77
+ }
78
+ let branchState = this.state.get(branch);
79
+ if (!branchState) {
80
+ throw new Error(`Branch ${branch} does not exist`);
81
+ }
82
+ const existing = branchState.get(key);
83
+ const now = new Date();
84
+ const version = existing ? existing.version + 1 : 1;
85
+ const hash = crypto.createHash('sha256').update(JSON.stringify(value)).digest('hex');
86
+ const entry = {
87
+ key,
88
+ value,
89
+ version,
90
+ hash,
91
+ created_at: existing?.created_at || now,
92
+ updated_at: now,
93
+ updated_by: agentId,
94
+ branch,
95
+ metadata: options?.metadata || existing?.metadata || {},
96
+ };
97
+ branchState.set(key, entry);
98
+ // Update branch head
99
+ const branchInfo = this.branches.get(branch);
100
+ branchInfo.head_version = Math.max(branchInfo.head_version, version);
101
+ // Log change
102
+ const change = {
103
+ type: 'set',
104
+ key,
105
+ old_value: existing?.value,
106
+ new_value: value,
107
+ version,
108
+ timestamp: now,
109
+ agent_id: agentId,
110
+ branch,
111
+ };
112
+ this.changeLog.push(change);
113
+ this.notifyChange(change);
114
+ return entry;
115
+ }
116
+ /**
117
+ * Delete a value
118
+ */
119
+ async delete(key, options) {
120
+ const branch = options?.branch || 'main';
121
+ const agentId = options?.agent_id || 'unknown';
122
+ const branchState = this.state.get(branch);
123
+ if (!branchState)
124
+ return false;
125
+ const existing = branchState.get(key);
126
+ if (!existing)
127
+ return false;
128
+ // Check for lock
129
+ const lock = this.locks.get(key);
130
+ if (lock && lock.type === 'exclusive' && lock.holder !== agentId) {
131
+ throw new Error(`Key ${key} is exclusively locked by ${lock.holder}`);
132
+ }
133
+ branchState.delete(key);
134
+ // Log change
135
+ const change = {
136
+ type: 'delete',
137
+ key,
138
+ old_value: existing.value,
139
+ version: existing.version,
140
+ timestamp: new Date(),
141
+ agent_id: agentId,
142
+ branch,
143
+ };
144
+ this.changeLog.push(change);
145
+ this.notifyChange(change);
146
+ return true;
147
+ }
148
+ /**
149
+ * List all keys
150
+ */
151
+ async keys(options) {
152
+ const branch = options?.branch || 'main';
153
+ const branchState = this.state.get(branch);
154
+ if (!branchState)
155
+ return [];
156
+ let keys = Array.from(branchState.keys());
157
+ if (options?.prefix) {
158
+ keys = keys.filter(k => k.startsWith(options.prefix));
159
+ }
160
+ return keys;
161
+ }
162
+ /**
163
+ * Check if key exists
164
+ */
165
+ async has(key, options) {
166
+ const branch = options?.branch || 'main';
167
+ const branchState = this.state.get(branch);
168
+ return branchState?.has(key) || false;
169
+ }
170
+ // ===========================================================================
171
+ // Locking
172
+ // ===========================================================================
173
+ /**
174
+ * Acquire a lock
175
+ */
176
+ async acquireLock(request) {
177
+ const { key, holder, type = 'exclusive', duration_ms = this.defaultLockDuration, wait_ms = 0 } = request;
178
+ const existingLock = this.locks.get(key);
179
+ // Check if already locked
180
+ if (existingLock) {
181
+ // Check if expired
182
+ if (new Date() > existingLock.expires_at) {
183
+ this.locks.delete(key);
184
+ }
185
+ else {
186
+ // Locked by someone else
187
+ if (existingLock.holder !== holder) {
188
+ // Can acquire shared lock if existing is shared
189
+ if (type === 'shared' && existingLock.type === 'shared') {
190
+ // Allow multiple shared locks (simplified: just extend)
191
+ }
192
+ else {
193
+ // Wait or fail
194
+ if (wait_ms > 0) {
195
+ return {
196
+ acquired: false,
197
+ reason: 'Key is locked',
198
+ current_holder: existingLock.holder,
199
+ retry_after_ms: Math.min(wait_ms, existingLock.expires_at.getTime() - Date.now()),
200
+ };
201
+ }
202
+ return {
203
+ acquired: false,
204
+ reason: 'Key is locked',
205
+ current_holder: existingLock.holder,
206
+ };
207
+ }
208
+ }
209
+ else {
210
+ // Same holder - renew
211
+ existingLock.expires_at = new Date(Date.now() + duration_ms);
212
+ existingLock.renewed_count++;
213
+ return { acquired: true, lock: existingLock };
214
+ }
215
+ }
216
+ }
217
+ // Create new lock
218
+ const lock = {
219
+ id: `lock_${crypto.randomUUID()}`,
220
+ key,
221
+ type,
222
+ holder,
223
+ acquired_at: new Date(),
224
+ expires_at: new Date(Date.now() + duration_ms),
225
+ renewed_count: 0,
226
+ };
227
+ this.locks.set(key, lock);
228
+ return { acquired: true, lock };
229
+ }
230
+ /**
231
+ * Release a lock
232
+ */
233
+ async releaseLock(key, holder) {
234
+ const lock = this.locks.get(key);
235
+ if (!lock)
236
+ return false;
237
+ if (lock.holder !== holder) {
238
+ throw new Error(`Lock on ${key} is held by ${lock.holder}, not ${holder}`);
239
+ }
240
+ this.locks.delete(key);
241
+ return true;
242
+ }
243
+ /**
244
+ * Renew a lock
245
+ */
246
+ async renewLock(key, holder, duration_ms) {
247
+ const lock = this.locks.get(key);
248
+ if (!lock) {
249
+ return { acquired: false, reason: 'Lock not found' };
250
+ }
251
+ if (lock.holder !== holder) {
252
+ return { acquired: false, reason: 'Lock held by another holder', current_holder: lock.holder };
253
+ }
254
+ lock.expires_at = new Date(Date.now() + (duration_ms || this.defaultLockDuration));
255
+ lock.renewed_count++;
256
+ return { acquired: true, lock };
257
+ }
258
+ /**
259
+ * Check if key is locked
260
+ */
261
+ isLocked(key) {
262
+ const lock = this.locks.get(key);
263
+ if (!lock) {
264
+ return { locked: false };
265
+ }
266
+ if (new Date() > lock.expires_at) {
267
+ this.locks.delete(key);
268
+ return { locked: false };
269
+ }
270
+ return { locked: true, holder: lock.holder, expires_at: lock.expires_at };
271
+ }
272
+ /**
273
+ * Get all locks held by an agent
274
+ */
275
+ getLocksForHolder(holder) {
276
+ return Array.from(this.locks.values()).filter(l => l.holder === holder);
277
+ }
278
+ /**
279
+ * Release all locks held by an agent
280
+ */
281
+ async releaseAllLocks(holder) {
282
+ let released = 0;
283
+ for (const [key, lock] of this.locks) {
284
+ if (lock.holder === holder) {
285
+ this.locks.delete(key);
286
+ released++;
287
+ }
288
+ }
289
+ return released;
290
+ }
291
+ // ===========================================================================
292
+ // Branching
293
+ // ===========================================================================
294
+ /**
295
+ * Create a branch
296
+ */
297
+ async createBranch(name, options) {
298
+ if (this.branches.has(name)) {
299
+ throw new Error(`Branch ${name} already exists`);
300
+ }
301
+ const parent = options?.parent || 'main';
302
+ const parentBranch = this.branches.get(parent);
303
+ if (!parentBranch) {
304
+ throw new Error(`Parent branch ${parent} does not exist`);
305
+ }
306
+ const parentState = this.state.get(parent);
307
+ // Create branch info
308
+ const branch = {
309
+ name,
310
+ parent,
311
+ created_at: new Date(),
312
+ created_by: options?.created_by || 'unknown',
313
+ head_version: parentBranch.head_version,
314
+ merged: false,
315
+ };
316
+ this.branches.set(name, branch);
317
+ // Copy state from parent
318
+ const branchState = new Map();
319
+ for (const [key, entry] of parentState) {
320
+ branchState.set(key, { ...entry, branch: name });
321
+ }
322
+ this.state.set(name, branchState);
323
+ return branch;
324
+ }
325
+ /**
326
+ * List branches
327
+ */
328
+ listBranches() {
329
+ return Array.from(this.branches.values());
330
+ }
331
+ /**
332
+ * Get branch info
333
+ */
334
+ getBranch(name) {
335
+ return this.branches.get(name);
336
+ }
337
+ /**
338
+ * Merge branch into target
339
+ */
340
+ async merge(source, target, options) {
341
+ const sourceBranch = this.branches.get(source);
342
+ const targetBranch = this.branches.get(target);
343
+ if (!sourceBranch)
344
+ throw new Error(`Source branch ${source} does not exist`);
345
+ if (!targetBranch)
346
+ throw new Error(`Target branch ${target} does not exist`);
347
+ if (sourceBranch.merged)
348
+ throw new Error(`Branch ${source} already merged`);
349
+ const sourceState = this.state.get(source);
350
+ const targetState = this.state.get(target);
351
+ const conflicts = [];
352
+ const mergedKeys = [];
353
+ const resolution = options?.resolution || this.conflictResolution;
354
+ // Find all keys
355
+ const allKeys = new Set([...sourceState.keys(), ...targetState.keys()]);
356
+ for (const key of allKeys) {
357
+ const sourceEntry = sourceState.get(key);
358
+ const targetEntry = targetState.get(key);
359
+ // Key only in source - add to target
360
+ if (sourceEntry && !targetEntry) {
361
+ targetState.set(key, { ...sourceEntry, branch: target });
362
+ mergedKeys.push(key);
363
+ continue;
364
+ }
365
+ // Key only in target - keep
366
+ if (!sourceEntry && targetEntry) {
367
+ continue;
368
+ }
369
+ // Both have key - check for conflict
370
+ if (sourceEntry && targetEntry) {
371
+ if (sourceEntry.hash === targetEntry.hash) {
372
+ // Same value, no conflict
373
+ continue;
374
+ }
375
+ // Conflict!
376
+ const conflict = {
377
+ key,
378
+ source_value: sourceEntry.value,
379
+ target_value: targetEntry.value,
380
+ source_version: sourceEntry.version,
381
+ target_version: targetEntry.version,
382
+ };
383
+ // Apply resolution strategy
384
+ if (resolution === 'last-write-wins') {
385
+ if (sourceEntry.updated_at > targetEntry.updated_at) {
386
+ targetState.set(key, { ...sourceEntry, branch: target, version: targetEntry.version + 1 });
387
+ mergedKeys.push(key);
388
+ }
389
+ // else keep target
390
+ }
391
+ else if (resolution === 'first-write-wins') {
392
+ if (sourceEntry.updated_at < targetEntry.updated_at) {
393
+ targetState.set(key, { ...sourceEntry, branch: target, version: targetEntry.version + 1 });
394
+ mergedKeys.push(key);
395
+ }
396
+ // else keep target
397
+ }
398
+ else if (resolution === 'merge') {
399
+ // Try to merge objects
400
+ if (typeof sourceEntry.value === 'object' && typeof targetEntry.value === 'object') {
401
+ const merged = { ...targetEntry.value, ...sourceEntry.value };
402
+ targetState.set(key, {
403
+ ...targetEntry,
404
+ value: merged,
405
+ version: targetEntry.version + 1,
406
+ updated_at: new Date(),
407
+ hash: crypto.createHash('sha256').update(JSON.stringify(merged)).digest('hex'),
408
+ });
409
+ mergedKeys.push(key);
410
+ }
411
+ else {
412
+ conflicts.push(conflict);
413
+ }
414
+ }
415
+ else {
416
+ // Manual resolution needed
417
+ conflicts.push(conflict);
418
+ }
419
+ }
420
+ }
421
+ // Mark source as merged if no conflicts
422
+ if (conflicts.length === 0) {
423
+ sourceBranch.merged = true;
424
+ sourceBranch.merged_at = new Date();
425
+ }
426
+ return {
427
+ success: conflicts.length === 0,
428
+ conflicts,
429
+ merged_keys: mergedKeys,
430
+ source_branch: source,
431
+ target_branch: target,
432
+ };
433
+ }
434
+ /**
435
+ * Resolve conflicts manually
436
+ */
437
+ async resolveConflicts(conflicts, resolutions, options) {
438
+ if (!options?.source || !options?.target) {
439
+ throw new Error('Source and target branches required');
440
+ }
441
+ const sourceState = this.state.get(options.source);
442
+ const targetState = this.state.get(options.target);
443
+ if (!sourceState || !targetState) {
444
+ throw new Error('Invalid branches');
445
+ }
446
+ for (const conflict of conflicts) {
447
+ const resolution = resolutions.get(conflict.key);
448
+ if (!resolution)
449
+ continue;
450
+ const targetEntry = targetState.get(conflict.key);
451
+ const sourceEntry = sourceState.get(conflict.key);
452
+ if (!targetEntry)
453
+ continue;
454
+ let newValue;
455
+ if (resolution === 'source' && sourceEntry) {
456
+ newValue = sourceEntry.value;
457
+ }
458
+ else if (resolution === 'target') {
459
+ continue; // Keep target
460
+ }
461
+ else {
462
+ newValue = resolution; // Custom value
463
+ }
464
+ targetState.set(conflict.key, {
465
+ ...targetEntry,
466
+ value: newValue,
467
+ version: targetEntry.version + 1,
468
+ updated_at: new Date(),
469
+ updated_by: options.agent_id || 'unknown',
470
+ hash: crypto.createHash('sha256').update(JSON.stringify(newValue)).digest('hex'),
471
+ });
472
+ }
473
+ // Mark source as merged
474
+ const sourceBranch = this.branches.get(options.source);
475
+ if (sourceBranch) {
476
+ sourceBranch.merged = true;
477
+ sourceBranch.merged_at = new Date();
478
+ }
479
+ }
480
+ /**
481
+ * Delete a branch
482
+ */
483
+ async deleteBranch(name) {
484
+ if (name === 'main') {
485
+ throw new Error('Cannot delete main branch');
486
+ }
487
+ const branch = this.branches.get(name);
488
+ if (!branch)
489
+ return false;
490
+ this.branches.delete(name);
491
+ this.state.delete(name);
492
+ return true;
493
+ }
494
+ // ===========================================================================
495
+ // Snapshots
496
+ // ===========================================================================
497
+ /**
498
+ * Create a snapshot
499
+ */
500
+ async createSnapshot(options) {
501
+ const branch = options?.branch || 'main';
502
+ const branchState = this.state.get(branch);
503
+ const branchInfo = this.branches.get(branch);
504
+ if (!branchState || !branchInfo) {
505
+ throw new Error(`Branch ${branch} does not exist`);
506
+ }
507
+ const snapshot = {
508
+ id: `snap_${crypto.randomUUID()}`,
509
+ branch,
510
+ timestamp: new Date(),
511
+ entries: new Map(branchState),
512
+ version: branchInfo.head_version,
513
+ };
514
+ this.snapshots.set(snapshot.id, snapshot);
515
+ return snapshot;
516
+ }
517
+ /**
518
+ * Restore from snapshot
519
+ */
520
+ async restoreSnapshot(snapshotId) {
521
+ const snapshot = this.snapshots.get(snapshotId);
522
+ if (!snapshot) {
523
+ throw new Error(`Snapshot ${snapshotId} not found`);
524
+ }
525
+ // Replace branch state
526
+ this.state.set(snapshot.branch, new Map(snapshot.entries));
527
+ // Update branch version
528
+ const branch = this.branches.get(snapshot.branch);
529
+ if (branch) {
530
+ branch.head_version = snapshot.version;
531
+ }
532
+ }
533
+ /**
534
+ * List snapshots
535
+ */
536
+ listSnapshots(branch) {
537
+ const snapshots = Array.from(this.snapshots.values());
538
+ if (branch) {
539
+ return snapshots.filter(s => s.branch === branch);
540
+ }
541
+ return snapshots;
542
+ }
543
+ /**
544
+ * Delete a snapshot
545
+ */
546
+ deleteSnapshot(snapshotId) {
547
+ return this.snapshots.delete(snapshotId);
548
+ }
549
+ // ===========================================================================
550
+ // Change Tracking
551
+ // ===========================================================================
552
+ /**
553
+ * Get change history
554
+ */
555
+ getChanges(options) {
556
+ let changes = [...this.changeLog];
557
+ if (options?.branch) {
558
+ changes = changes.filter(c => c.branch === options.branch);
559
+ }
560
+ if (options?.key) {
561
+ changes = changes.filter(c => c.key === options.key);
562
+ }
563
+ if (options?.agent_id) {
564
+ changes = changes.filter(c => c.agent_id === options.agent_id);
565
+ }
566
+ if (options?.since) {
567
+ changes = changes.filter(c => c.timestamp >= options.since);
568
+ }
569
+ // Sort by timestamp descending
570
+ changes.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
571
+ if (options?.limit) {
572
+ changes = changes.slice(0, options.limit);
573
+ }
574
+ return changes;
575
+ }
576
+ /**
577
+ * Subscribe to changes
578
+ */
579
+ onChange(callback) {
580
+ this.changeCallbacks.push(callback);
581
+ return () => {
582
+ const index = this.changeCallbacks.indexOf(callback);
583
+ if (index !== -1)
584
+ this.changeCallbacks.splice(index, 1);
585
+ };
586
+ }
587
+ notifyChange(change) {
588
+ for (const cb of this.changeCallbacks) {
589
+ try {
590
+ cb(change);
591
+ }
592
+ catch (e) {
593
+ // Ignore
594
+ }
595
+ }
596
+ }
597
+ // ===========================================================================
598
+ // Utilities
599
+ // ===========================================================================
600
+ cleanupExpiredLocks() {
601
+ const now = new Date();
602
+ for (const [key, lock] of this.locks) {
603
+ if (now > lock.expires_at) {
604
+ this.locks.delete(key);
605
+ }
606
+ }
607
+ }
608
+ /**
609
+ * Get state statistics
610
+ */
611
+ getStats() {
612
+ let totalKeys = 0;
613
+ for (const branchState of this.state.values()) {
614
+ totalKeys += branchState.size;
615
+ }
616
+ return {
617
+ branches: this.branches.size,
618
+ total_keys: totalKeys,
619
+ active_locks: this.locks.size,
620
+ snapshots: this.snapshots.size,
621
+ changes_logged: this.changeLog.length,
622
+ };
623
+ }
624
+ /**
625
+ * Export state for backup
626
+ */
627
+ async exportState(branch) {
628
+ const exportData = {
629
+ exported_at: new Date(),
630
+ branches: branch ? [branch] : Array.from(this.branches.keys()),
631
+ data: {},
632
+ };
633
+ for (const branchName of exportData.branches) {
634
+ const branchState = this.state.get(branchName);
635
+ if (branchState) {
636
+ exportData.data[branchName] = Object.fromEntries(branchState);
637
+ }
638
+ }
639
+ return JSON.stringify(exportData, null, 2);
640
+ }
641
+ /**
642
+ * Import state from backup
643
+ */
644
+ async importState(data, options) {
645
+ const importData = JSON.parse(data);
646
+ let importedKeys = 0;
647
+ for (const branchName of Object.keys(importData.data)) {
648
+ if (!this.branches.has(branchName) && branchName !== 'main') {
649
+ await this.createBranch(branchName);
650
+ }
651
+ const branchState = this.state.get(branchName);
652
+ const entries = importData.data[branchName];
653
+ for (const [key, entry] of Object.entries(entries)) {
654
+ if (options?.merge && branchState.has(key)) {
655
+ continue; // Skip existing
656
+ }
657
+ branchState.set(key, entry);
658
+ importedKeys++;
659
+ }
660
+ }
661
+ return { imported_keys: importedKeys };
662
+ }
663
+ }
664
+ export default StateManager;
665
+ //# sourceMappingURL=state.js.map