@ibgib/core-gib 0.1.19 → 0.1.20

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.
Files changed (70) hide show
  1. package/dist/common/other/ibgib-helper.d.mts +13 -0
  2. package/dist/common/other/ibgib-helper.d.mts.map +1 -1
  3. package/dist/common/other/ibgib-helper.mjs +44 -0
  4. package/dist/common/other/ibgib-helper.mjs.map +1 -1
  5. package/dist/sync/merge-info/merge-info-constants.d.mts +2 -0
  6. package/dist/sync/merge-info/merge-info-constants.d.mts.map +1 -0
  7. package/dist/sync/merge-info/merge-info-constants.mjs +2 -0
  8. package/dist/sync/merge-info/merge-info-constants.mjs.map +1 -0
  9. package/dist/sync/merge-info/merge-info-helpers.d.mts +51 -0
  10. package/dist/sync/merge-info/merge-info-helpers.d.mts.map +1 -0
  11. package/dist/sync/merge-info/merge-info-helpers.mjs +92 -0
  12. package/dist/sync/merge-info/merge-info-helpers.mjs.map +1 -0
  13. package/dist/sync/merge-info/merge-info-helpers.respec.d.mts +2 -0
  14. package/dist/sync/merge-info/merge-info-helpers.respec.d.mts.map +1 -0
  15. package/dist/sync/merge-info/merge-info-helpers.respec.mjs +32 -0
  16. package/dist/sync/merge-info/merge-info-helpers.respec.mjs.map +1 -0
  17. package/dist/sync/merge-info/merge-info-types.d.mts +26 -0
  18. package/dist/sync/merge-info/merge-info-types.d.mts.map +1 -0
  19. package/dist/sync/merge-info/merge-info-types.mjs +2 -0
  20. package/dist/sync/merge-info/merge-info-types.mjs.map +1 -0
  21. package/dist/sync/strategies/conflict-optimistic.d.mts +37 -0
  22. package/dist/sync/strategies/conflict-optimistic.d.mts.map +1 -0
  23. package/dist/sync/strategies/conflict-optimistic.mjs +162 -0
  24. package/dist/sync/strategies/conflict-optimistic.mjs.map +1 -0
  25. package/dist/sync/sync-conflict.respec.d.mts +8 -0
  26. package/dist/sync/sync-conflict.respec.d.mts.map +1 -0
  27. package/dist/sync/sync-conflict.respec.mjs +158 -0
  28. package/dist/sync/sync-conflict.respec.mjs.map +1 -0
  29. package/dist/sync/sync-innerspace-constants.respec.mjs +2 -2
  30. package/dist/sync/sync-innerspace-constants.respec.mjs.map +1 -1
  31. package/dist/sync/sync-innerspace-deep-updates.respec.mjs +0 -1
  32. package/dist/sync/sync-innerspace-deep-updates.respec.mjs.map +1 -1
  33. package/dist/sync/sync-innerspace.respec.mjs +1 -1
  34. package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
  35. package/dist/sync/sync-saga-coordinator.d.mts +23 -12
  36. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  37. package/dist/sync/sync-saga-coordinator.mjs +473 -107
  38. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  39. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts +11 -0
  40. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts.map +1 -1
  41. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs +24 -0
  42. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs.map +1 -1
  43. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +23 -0
  44. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -1
  45. package/dist/sync/sync-types.d.mts +31 -4
  46. package/dist/sync/sync-types.d.mts.map +1 -1
  47. package/dist/sync/sync-types.mjs.map +1 -1
  48. package/ibgib-foundations.md +129 -0
  49. package/package.json +1 -1
  50. package/roadmap.md +59 -0
  51. package/src/common/other/ibgib-helper.mts +52 -0
  52. package/src/keystone/README.md +13 -155
  53. package/src/keystone/docs/architecture.md +55 -0
  54. package/src/sync/README.md +37 -42
  55. package/src/sync/docs/architecture.md +69 -0
  56. package/src/sync/docs/verification.md +43 -0
  57. package/src/sync/merge-info/merge-info-constants.mts +1 -0
  58. package/src/sync/merge-info/merge-info-helpers.mts +134 -0
  59. package/src/sync/merge-info/merge-info-helpers.respec.mts +41 -0
  60. package/src/sync/merge-info/merge-info-types.mts +28 -0
  61. package/src/sync/strategies/conflict-optimistic.mts +208 -0
  62. package/src/sync/sync-conflict.respec.mts +194 -0
  63. package/src/sync/sync-innerspace-constants.respec.mts +1 -1
  64. package/src/sync/sync-innerspace-deep-updates.respec.mts +0 -1
  65. package/src/sync/sync-innerspace.respec.mts +1 -1
  66. package/src/sync/sync-saga-coordinator.mts +524 -118
  67. package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +41 -0
  68. package/src/sync/sync-saga-message/sync-saga-message-types.mts +23 -0
  69. package/src/sync/sync-types.mts +33 -4
  70. package/tmp.md +2 -425
@@ -23,6 +23,7 @@ import { appendToTimeline, createTimeline, Rel8nInfo, Rel8nRemovalInfo } from ".
23
23
  import {
24
24
  SyncData_V1, SyncIbGib_V1, SyncInitData, SyncConflictStrategy,
25
25
  SyncMode,
26
+ SyncOptions,
26
27
  } from "./sync-types.mjs";
27
28
  import { getSyncIb, isPastFrame } from "./sync-helpers.mjs";
28
29
  import { getSyncSagaDependencyGraph } from "./sync-helpers.mjs";
@@ -41,6 +42,9 @@ import { createSyncSagaContext } from "./sync-saga-context/sync-saga-context-hel
41
42
  import { newupSubject } from "../common/pubsub/subject/subject-helper.mjs";
42
43
  import { SubjectWitness } from "../common/pubsub/subject/subject-types.mjs";
43
44
 
45
+ import { mergeDivergentTimelines } from "./strategies/conflict-optimistic.mjs";
46
+ import { getSyncSagaMessageFromFrame } from "./sync-saga-message/sync-saga-message-helpers.mjs";
47
+
44
48
 
45
49
  const logalot = GLOBAL_LOG_A_LOT || true;
46
50
 
@@ -81,20 +85,19 @@ export class SyncSagaCoordinator {
81
85
  */
82
86
  async sync({
83
87
  peer,
84
- localSpace,
88
+ localSpace: _localSpace,
89
+ source: _source,
85
90
  metaspace,
86
91
  domainIbGibs,
87
- useSessionIdentity,
88
- }: {
89
- peer: SyncPeerWitness,
90
- localSpace: IbGibSpaceAny,
91
- metaspace: MetaspaceService,
92
- domainIbGibs: IbGib_V1[],
93
- useSessionIdentity: boolean,
94
- }): Promise<SyncSagaInfo> {
92
+ conflictStrategy = 'abort',
93
+ useSessionIdentity = true,
94
+ }: SyncOptions): Promise<SyncSagaInfo> {
95
95
  const lc = `${this.lc}[${this.sync.name}]`;
96
96
  if (logalot) { console.log(`${lc} starting...`); }
97
97
 
98
+ const localSpace = (_source || _localSpace)!;
99
+ if (!localSpace) { throw new Error(`${lc} source (or localSpace) required (E: 8a9b0c1d)`); }
100
+
98
101
  // 1. SETUP SAGA METADATA
99
102
  const sagaId = await getUUID();
100
103
 
@@ -150,6 +153,7 @@ export class SyncSagaCoordinator {
150
153
  domainIbGibs,
151
154
  tempSpace,
152
155
  metaspace,
156
+ conflictStrategy
153
157
  });
154
158
 
155
159
  // 4. EXECUTE SAGA LOOP (FSM)
@@ -337,6 +341,17 @@ export class SyncSagaCoordinator {
337
341
 
338
342
  // C. Handle Response
339
343
  if (!responseCtx) {
344
+ // Check if we just sent a Commit frame. If so, peer's silence is success/expected.
345
+ if (currentFrame) {
346
+ const msg = await getSyncSagaMessageFromFrame({ frameIbGib: currentFrame, space: localSpace });
347
+ if (logalot) { console.log(`${lc} Checking currentFrame stage: ${msg?.data?.stage} (Expected: ${SyncStage.commit})`); }
348
+ if (msg?.data?.stage === SyncStage.commit) {
349
+ if (logalot) { console.log(`${lc} Sender sent Commit. Peer returned no response. Saga Complete.`); }
350
+ currentFrame = null;
351
+ break;
352
+ }
353
+ }
354
+
340
355
  throw new Error(`responseCtx falsy. Peer returned no response context (E: c099d8073b48d85e881f917835158f26)`);
341
356
  // console.warn(`${lc} Peer returned no response context. Ending loop.`);
342
357
  // currentFrame = null;
@@ -397,6 +412,49 @@ export class SyncSagaCoordinator {
397
412
  return allReceivedIbGibs;
398
413
  }
399
414
 
415
+ /**
416
+ * Helper to get Knowledge Vector for specific domain ibGibs or TJPs.
417
+ * Useful for testing and external validation.
418
+ */
419
+ public async getKnowledgeVector({
420
+ space,
421
+ metaspace,
422
+ domainIbGibs,
423
+ tjpAddrs,
424
+ }: {
425
+ space: IbGibSpaceAny,
426
+ metaspace: MetaspaceService,
427
+ domainIbGibs?: IbGib_V1[],
428
+ tjpAddrs?: string[],
429
+ }): Promise<{ [tjp: string]: string | null }> {
430
+ const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
431
+ if (logalot) { console.log(`${lc} starting...`); }
432
+
433
+ let tjps: string[] = [];
434
+ if (tjpAddrs) {
435
+ tjps = tjpAddrs;
436
+ } else if (domainIbGibs && domainIbGibs.length > 0) {
437
+ // Extract TJPs from domain Ibgibs
438
+ const { mapWithTjp_YesDna, mapWithTjp_NoDna } = splitPerTjpAndOrDna({ ibGibs: domainIbGibs });
439
+ const allWithTjp = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
440
+ const timelineMap = getTimelinesGroupedByTjp({ ibGibs: allWithTjp });
441
+ tjps = Object.keys(timelineMap);
442
+ } else {
443
+ // No info provided. Return empty? Or throw?
444
+ // User test context implied "everything", but implementation requires scope.
445
+ console.warn(`${lc} No domainIbGibs or tjpAddrs provided. Returning empty KV.`);
446
+ return {};
447
+ }
448
+
449
+ if (tjps.length === 0) { return {}; }
450
+
451
+ const res = await getLatestAddrs({ space, tjpAddrs: tjps });
452
+ if (!res.data || !res.data.latestAddrsMap) {
453
+ throw new Error(`${lc} Failed to get latest addrs. (E: 7a8b9c0d)`);
454
+ }
455
+ return res.data.latestAddrsMap;
456
+ }
457
+
400
458
  protected async analyzeTimelines({
401
459
  domainIbGibs,
402
460
  space,
@@ -450,14 +508,16 @@ export class SyncSagaCoordinator {
450
508
  localSpace,
451
509
  domainIbGibs,
452
510
  tempSpace,
453
- metaspace
511
+ metaspace,
512
+ conflictStrategy,
454
513
  }: {
455
514
  sagaId: string,
456
515
  sessionIdentity?: KeystoneIbGib_V1,
457
516
  localSpace: IbGibSpaceAny,
458
517
  domainIbGibs: IbGib_V1[],
459
518
  tempSpace: IbGibSpaceAny,
460
- metaspace: MetaspaceService
519
+ metaspace: MetaspaceService,
520
+ conflictStrategy: SyncConflictStrategy,
461
521
  }): Promise<{ sagaFrame: SyncIbGib_V1, srcGraph: { [addr: string]: IbGib_V1 } }> {
462
522
  const lc = `${this.lc}[${this.createInitFrame.name}]`;
463
523
  try {
@@ -496,7 +556,8 @@ export class SyncSagaCoordinator {
496
556
  msgStones: [initStone],
497
557
  identity: sessionIdentity,
498
558
  space: tempSpace,
499
- metaspace
559
+ metaspace,
560
+ conflictStrategy,
500
561
  });
501
562
  if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
502
563
 
@@ -552,19 +613,19 @@ export class SyncSagaCoordinator {
552
613
 
553
614
  switch (stage) {
554
615
  case SyncStage.init:
555
- return await this.handleInitFrame({ sagaIbGib, messageData, space, metaspace, identity, identitySecret });
616
+ return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, space, identity, identitySecret });
556
617
 
557
618
  case SyncStage.ack:
558
- return await this.handleAckFrame({ sagaIbGib, srcGraph, space, metaspace, identity });
619
+ return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, space, identity });
559
620
 
560
621
  case SyncStage.delta:
561
- return await this.handleDeltaFrame({ sagaIbGib, srcGraph, space, identity, metaspace });
622
+ return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, space, identity, });
562
623
 
563
624
  case SyncStage.commit:
564
- return await this.handleCommitFrame({ sagaIbGib, space });
625
+ return await this.handleCommitFrame({ sagaIbGib, metaspace, space, identity, });
565
626
 
566
627
  case SyncStage.conflict:
567
- return await this.handleConflictFrame({ sagaIbGib, space });
628
+ return await this.handleConflictFrame({ sagaIbGib, metaspace, space, });
568
629
 
569
630
  default:
570
631
  throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
@@ -611,14 +672,19 @@ export class SyncSagaCoordinator {
611
672
 
612
673
  // Extract Init Data
613
674
  const initData = messageData as SyncSagaMessageInitData_V1; // Using renamed variable for clarity
675
+ if (initData.stage !== SyncStage.init) {
676
+ throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
677
+ }
614
678
  if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
615
679
  if (!initData || !initData.knowledgeVector) {
616
680
  throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
617
681
  }
618
682
 
683
+ // Determine Strategy from Saga Data (since V1 stores it in root)
684
+ const conflictStrategy = sagaIbGib.data!.conflictStrategy || 'abort';
685
+
619
686
  // 2. Gap Analysis
620
- const conflicts: { tjp: string, localAddr?: string, remoteAddr: string, reason: string }[] = [];
621
- const conflictStrategy: SyncConflictStrategy = initData.conflictStrategy || 'abort'; // Default to abort if not specified, or we should parameterize this
687
+ const conflicts: { tjpAddr: string, localAddr: string, remoteAddr: string, timelineAddrs: string[], reason: string, terminal: boolean }[] = [];
622
688
 
623
689
  const deltaReqAddrs: string[] = [];
624
690
  const pushOfferAddrs: string[] = [];
@@ -703,21 +769,47 @@ export class SyncSagaCoordinator {
703
769
  deltaReqAddrs.push(remoteAddr);
704
770
  } else {
705
771
  // DIVERGENCE: Both have changes the other doesn't know about.
706
- conflicts.push({ tjp, localAddr, remoteAddr, reason: 'divergence' });
707
-
708
- // Conflict Strategy Handling
709
- // TODO: Implement "manual" strategy (requires user intervention/pause)
710
- // TODO: Implement "local_wins" strategy
711
- // TODO: Implement "server_wins" strategy
712
772
 
713
773
  if (conflictStrategy === 'abort') {
714
- // We will abort immediately after this loop check if any conflicts exist
774
+ // Abort Strategy: We will treat this as terminal.
775
+ // But for Unified Ack, we just mark it terminal in the list?
776
+ // Or do we actually throw/abort the saga?
777
+ // Current logic (below) aborts the saga if ANY conflict is terminal/abort.
778
+ conflicts.push({
779
+ tjpAddr: tjp,
780
+ localAddr: localAddr!,
781
+ remoteAddr,
782
+ timelineAddrs: [], // Not needed for abort
783
+ reason: 'divergence',
784
+ terminal: true
785
+ });
715
786
  } else if (conflictStrategy === 'optimistic') {
716
- // Optimistic: We Try to resolve by syncing timeline-by-timeline.
717
- // We request the remote data (deltaReq) and will attempt to merge later?
718
- // OR we treat it as a delta request for now, hoping the merge logic handles it?
719
- // For now, we request the remote tip.
720
- deltaReqAddrs.push(remoteAddr);
787
+ // Optimistic: We want to resolving this.
788
+ // We need to send our history to the Sender so they can Merge.
789
+
790
+ // Fetch Full History for Local Timeline
791
+ // Note: We might optimize this to only send "recent" history if we had a KV?
792
+ // But for now, get full past.
793
+ // Optimization: localKV might not have full history.
794
+ // We need to inspect the 'past' of the local tip.
795
+
796
+ // We need the ACTUAL object to get the past.
797
+ // We have localAddr.
798
+ const resLocalTip = await getFromSpace({ space, addr: localAddr! });
799
+ const localTip = resLocalTip.ibGibs?.[0];
800
+ if (!localTip) { throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`); }
801
+
802
+ const timelineAddrs = [localAddr!, ...(localTip.rel8ns?.past || [])];
803
+
804
+ conflicts.push({
805
+ tjpAddr: tjp,
806
+ localAddr: localAddr!,
807
+ remoteAddr,
808
+ timelineAddrs,
809
+ reason: 'divergence',
810
+ terminal: false
811
+ });
812
+
721
813
  } else {
722
814
  throw new Error(`${lc} Unsupported conflict strategy: ${conflictStrategy} (E: 2a9b3c4d5e6f7g8h9i0j)`);
723
815
  }
@@ -725,34 +817,26 @@ export class SyncSagaCoordinator {
725
817
  }
726
818
  }
727
819
 
728
- if (conflicts.length > 0 && conflictStrategy === 'abort') {
820
+ // Check if we should ABORT (if any conflict is terminal)
821
+ const hasTerminalConflicts = conflicts.some(c => c.terminal);
822
+
823
+ if (hasTerminalConflicts) {
729
824
  // Abort Strategy: Kill the saga.
730
- if (logalot) { console.warn(`${lc} ABORTING Sync Saga due to conflicts: ${JSON.stringify(conflicts)}`); }
825
+ if (logalot) { console.warn(`${lc} ABORTING Sync Saga due to terminal conflicts: ${JSON.stringify(conflicts)}`); }
731
826
 
732
- const sagaId = sagaIbGib.data!.uuid;
733
- const conflictData: SyncSagaMessageConflictData_V1 = {
734
- sagaId,
735
- stage: SyncStage.conflict,
736
- conflictStrategy,
737
- isTerminal: true,
738
- conflicts,
739
- };
827
+ // We reuse the ConflictData structure for terminal aborts?
828
+ // Or do we send an Ack with terminal conflicts?
829
+ // Original design had explicit Conflict Frame for Abort.
830
+ // Let's stick to that for purely terminal cases to be safe/explicit?
831
+ // Or Unified: Just send Ack with terminal=true conflicts. Sender sees them and aborts.
740
832
 
741
- const conflictStone = await this.createSyncMsgStone({
742
- data: conflictData,
743
- space,
744
- metaspace,
745
- });
833
+ // Decision: Unified Ack for everything is cleaner protocol.
834
+ // But wait, the original code below creates a Conflict Stone.
835
+ // Let's preserve the explicit 'Conflict' frame for total aborts if that's easier,
836
+ // OR fully switch to Ack.
837
+ // Protocol states: Init -> Ack. If Ack contains terminal errors, Sender can Commit(Fail).
746
838
 
747
- const conflictFrame = await this.evolveSyncSagaIbGib({
748
- prevSagaIbGib: sagaIbGib,
749
- msgStones: [conflictStone],
750
- identity,
751
- space,
752
- metaspace,
753
- });
754
-
755
- return { frame: conflictFrame };
839
+ // Let's use Ack with conflicts.
756
840
  }
757
841
 
758
842
  // 2. Add Push Offers (Missing in Local)
@@ -817,27 +901,7 @@ export class SyncSagaCoordinator {
817
901
  // Problem: Receiver doesn't know X is related to Y without X.
818
902
 
819
903
  // SOLUTION:
820
- // The *Sender* must include TJP/Past info in `Init`? Or `pushOfferAddrs` should be richer?
821
- // OR: Receiver does a check.
822
- // Wait, if Sender sends V2, it's just an address.
823
- // If Receiver doesn't have it, it's opaque.
824
-
825
- // REVISIT "Constant / No TJP" logic:
826
- // IF we are testing "Sender Newer", meaning Sender has V2, Receiver has V1.
827
- // Sender calls `sync([V2])`. Init Frame contains `stones: [V2_Address]`.
828
- // Receiver checks V2_Address. Not found.
829
- // Receiver requests V2.
830
- // Receiver sends Ack(DeltaReq: [V2], Knowledge: {}).
831
- // Sender receives Ack. Sender sends V2 *AND* its deps (V1, Root).
832
- // Receiver has V1. Sender sends V1 anyway.
833
- // This is "Naive Deep Sync".
834
-
835
- // TO ACHIEVE "Smart Diff":
836
- // Receiver needs to know "Oh, V2 is a timeline tip of TJP_A".
837
- // If Sender doesn't send TJP info, Receiver is blind.
838
-
839
- // Proposed Fix (Short Term):
840
- // `SyncInitData` should include TJP mappings or we rely on `knowledgeVector` in `Init`?
904
+ // The *Sender* must include TJP mappings or we rely on `knowledgeVector` in `Init`?
841
905
  // `SyncSagaMessageInitData_V1` extends `SyncInitData`.
842
906
  // `SyncInitData` has `knowledgeVector`.
843
907
  // If Sender populates `knowledgeVector` in `Init`, Receiver can use keys (TJPs) to look up its own state!
@@ -914,6 +978,148 @@ export class SyncSagaCoordinator {
914
978
  if (ackData.stage !== SyncStage.ack) {
915
979
  throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
916
980
  }
981
+ if (logalot) { console.log(`${lc} ackData: ${pretty(ackData)} (I: 7f8e9d0a1b2c3d4e5f6g7h8i9j0k)`); }
982
+
983
+ // 1. Check for Conflicts
984
+ const conflicts = ackData.conflicts || [];
985
+ const terminalConflicts = conflicts.filter(c => c.terminal);
986
+ if (terminalConflicts.length > 0) {
987
+ console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`);
988
+ // Terminal failure. Sender should probably Commit(Fail) or just Abort.
989
+ // For now, throw to trigger abort.
990
+ throw new Error(`${lc} Peer reported terminal conflicts. (E: a1b2c3d4e5f6g7h8i9j0k)`);
991
+ }
992
+
993
+ const optimisticConflicts = conflicts.filter(c => !c.terminal);
994
+ const mergeDeltaReqs: string[] = []; // Additional requests for merging
995
+
996
+ if (optimisticConflicts.length > 0) {
997
+ if (logalot) { console.log(`${lc} Processing optimistic conflicts: ${optimisticConflicts.length}`); }
998
+ // We need to resolve these.
999
+ // Strategy:
1000
+ // 1. Analyze Divergence (Sender vs Receiver)
1001
+ // 2. Identify missing data needed for merge (Receiver's unique frames)
1002
+ // 3. Request that data (as Delta Reqs)
1003
+ // 4. (Later in Delta Phase) Perform Merge.
1004
+
1005
+ // BUT: The Delta Phase is usually generic "Send me these Addrs".
1006
+ // If we just add to `deltaReqAddrs` (which are requests for Sender to send to Receiver?),
1007
+ // wait. `ackData.deltaReqAddrs` are what RECEIVER wants from SENDER.
1008
+
1009
+ // We (Sender) are processing the Ack.
1010
+ // We need to request data FROM Receiver.
1011
+ // But the protocol 'Ack' step typically leads to 'Delta' (Sender sending data).
1012
+
1013
+ // If Sender needs data from Receiver, it usually happens in 'Pull' mode or a separate request?
1014
+ // Or can we send a 'Delta Request' frame?
1015
+ // Standard Saga: Init(Push) -> Ack(Pull Reqs) -> Delta(Push Data).
1016
+
1017
+ // If Sender needs data, we might need a "Reverse Delta" or "Pull" phase?
1018
+ // Or we just proceed to Delta (sending what Receiver wants),
1019
+ // AND we piggyback our own requests?
1020
+ // OR: We treat the Conflict Resolution as a sub-saga or side-effect?
1021
+
1022
+ // SIMPLIFICATION for V1:
1023
+ // If we need data to merge, we must get it.
1024
+ // We are the Coordinator (Active). We can fetch from Peer immediately?
1025
+ // `peer.pull(addr)`?
1026
+ // Yes! The Coordinator has the `peer`.
1027
+
1028
+ // Let's analyze and pull immediately.
1029
+
1030
+ for (const conflict of optimisticConflicts) {
1031
+ const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
1032
+
1033
+ // Sender History
1034
+ // We need our own history for this timeline.
1035
+ // We know the 'senderTip' (remoteAddr in Ack).
1036
+ // Sender should verify it has this tip.
1037
+
1038
+ // Compute Diffs
1039
+ // We need to find `receiverOnly` addrs.
1040
+ // Receiver sent us `timelineAddrs` (Full History).
1041
+ const receiverHistorySet = new Set(timelineAddrs);
1042
+
1043
+ // We need our execution context's history for this senderTip.
1044
+ // We can fetch valid 'past' from space.
1045
+ const resSenderTip = await getFromSpace({ space, addr: senderTip });
1046
+ const senderTipIbGib = resSenderTip.ibGibs?.[0];
1047
+ if (!senderTipIbGib) { throw new Error(`${lc} Sender missing its own tip? ${senderTip} (E: 9c8d7e6f5g4h3i2j1k0l)`); }
1048
+
1049
+ // Basic Diff: Find what Receiver has that we don't.
1050
+ // Actually, we need to traverse OUR past to find commonality.
1051
+ const senderHistory = [senderTip, ...(senderTipIbGib.rel8ns?.past || [])];
1052
+
1053
+ const receiverOnlyAddrs = timelineAddrs.filter(addr => !senderHistory.includes(addr));
1054
+
1055
+ if (receiverOnlyAddrs.length > 0) {
1056
+ if (logalot) { console.log(`${lc} Pulling divergent history from Receiver: ${receiverOnlyAddrs.length} frames`); }
1057
+ // PULL these frames from Peer into Local Space
1058
+ // (Validation: We trust peer for now / verification happens on put)
1059
+ for (const addr of receiverOnlyAddrs) {
1060
+ // This 'pull' is a sync-peer method?
1061
+ // The Coordinator 'peer' passed in 'sync()' might be needed here?
1062
+ // Wait, `handleAckFrame` doesn't have reference to `peer`?
1063
+ // It only has `space`, `metaspace`.
1064
+ // The `peer` is held by the `executeSagaLoop`.
1065
+
1066
+ // PROBLEM: `handleAckFrame` is pure logic on the Space/Data?
1067
+ // No, it's a method on Coordinator.
1068
+ // But `executeSagaLoop` calls it.
1069
+ // We might need to return "Requirements" to the loop?
1070
+
1071
+ // Checking return type: `{ frame: SyncIbGib_V1, payloadIbGibs?: ... }`
1072
+ // It returns the NEXT frame (Delta).
1073
+
1074
+ // If we need to fetch data, we are blocked.
1075
+ // We can't easily "Pull" here without the Peer reference.
1076
+
1077
+ // OPTION A: Pass `peer` to `handleAckFrame`.
1078
+ // OPTION B: Return a strict list of "MissingDeps" and let Loop handle it.
1079
+
1080
+ // Let's assume we can resolve this by adding `peer` to signature or using `metaspace` if it's a peer-witness?
1081
+ // No, Peer is ephemeral connection.
1082
+
1083
+ // Let's add `peer` to `handleAckFrame` signature?
1084
+ // It breaks the pattern of just handling frame + space.
1085
+
1086
+ // ALTERNATIVE: Use the `Delta` frame to request data?
1087
+ // `SyncSagaMessageDeltaData` has `requests?: string[]`.
1088
+ // Sender sends Delta Frame.
1089
+ // Does Receiver handle Delta Requests?
1090
+ // `handleDeltaFrame` (Receiver) -> checks `requests`.
1091
+ // YES.
1092
+
1093
+ // So Sender puts `receiverOnlyAddrs` into `deltaFrame.requests`.
1094
+ // Receiver sees them, fetches them, and includes them in the Response (Commit?).
1095
+ // Wait, Init->Ack->Delta->Commit.
1096
+ // If Receiver sends data in Commit, that's "too late" for Sender to Merge in THIS saga round?
1097
+ // Unless Commit is not the end?
1098
+
1099
+ // Or we do a "Delta 2" loop?
1100
+
1101
+ // "Iterative Resolution Loop" from plan.
1102
+ // If we request data in Delta, Receiver sends it in Commit (or Delta-Response).
1103
+ // Sender gets Commit. Sees data. Merges.
1104
+ // Then Sender needs to Send the MERGE result.
1105
+ // Needs another Push/Delta.
1106
+
1107
+ // REFINED FLOW:
1108
+ // 1. Sender sends Delta Frame with `requests: [receiverOnlyAddrs]`.
1109
+ // 2. Receiver responds (Commit? or Ack 2?) with Payload (Divergent Frames).
1110
+ // 3. Sender handles response -> Merges.
1111
+ // 4. Sender sends Commit (containing Merge Frame).
1112
+
1113
+ // Issue: Current state machine is Init->Ack->Delta->Commit.
1114
+ // We need to keep Saga open.
1115
+ // If Sender sends Delta with requests, does it transition to Commit?
1116
+ }
1117
+ mergeDeltaReqs.push(...receiverOnlyAddrs);
1118
+ }
1119
+ }
1120
+ }
1121
+
1122
+ // 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
917
1123
 
918
1124
  const deltaReqAddrs = ackData.deltaReqAddrs || [];
919
1125
  const pushOfferAddrs = ackData.pushOfferAddrs || [];
@@ -1003,10 +1209,10 @@ export class SyncSagaCoordinator {
1003
1209
  // 3. Create Delta Frame
1004
1210
  const sagaId = ackData.sagaId;
1005
1211
  const deltaData: SyncSagaMessageDeltaData_V1 = {
1006
- sagaId,
1212
+ sagaId: sagaIbGib.data!.uuid,
1007
1213
  stage: SyncStage.delta,
1008
1214
  payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
1009
- requests: pullReqAddrs.length > 0 ? pullReqAddrs : undefined,
1215
+ requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
1010
1216
  };
1011
1217
 
1012
1218
  const deltaStone = await this.createSyncMsgStone({
@@ -1061,25 +1267,37 @@ export class SyncSagaCoordinator {
1061
1267
  if (deltaData.stage !== SyncStage.delta) {
1062
1268
  throw new Error(`${lc} Invalid delta frame: deltaData.stage !== SyncStage.delta (E: 0c28c8d8f08a4421b8344e6727271421)`);
1063
1269
  }
1270
+ if (logalot) { console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`); }
1064
1271
 
1065
1272
  const payloadAddrs = deltaData.payloadAddrs || [];
1066
- const requests = deltaData.requests || [];
1273
+ const peerRequests = deltaData.requests || [];
1274
+ const peerProposesCommit = deltaData.proposeCommit || false;
1067
1275
 
1068
- // 1. Process Received Payload
1276
+ // 1. Process Received Payload (Ingest)
1069
1277
  const receivedPayloadIbGibs: IbGib_V1[] = [];
1070
1278
  if (payloadAddrs.length > 0) {
1279
+ // We use `payloadAddrs` as the manifest.
1280
+ // The ACTUAL collection of ibGibs should be available via `getFromSpace`
1281
+ // assuming the "Transport" layer put them there implicitly?
1282
+ // OR, if we are local-only, we just get them.
1283
+ // The `handleDeltaFrame` contract assumes data is reachable in `space`.
1284
+
1071
1285
  const res = await getFromSpace({
1072
1286
  addrs: payloadAddrs,
1073
1287
  space,
1074
1288
  });
1075
1289
  if (res.ibGibs) {
1076
1290
  receivedPayloadIbGibs.push(...res.ibGibs);
1291
+ // Also put them? `getFromSpace` retrieves. If they are in space, they are persisted.
1292
+ // If this is a Temp Space, they are safe.
1293
+ } else {
1294
+ console.warn(`${lc} Failed to retrieve payloads listed in delta: ${payloadAddrs.join(', ')}`);
1077
1295
  }
1078
1296
  }
1079
1297
 
1080
- // 2. Fulfill Requests (Outgoing Payload)
1298
+ // 2. Fulfill Peer Requests (Outgoing Payload)
1081
1299
  const outgoingPayload: IbGib_V1[] = [];
1082
- for (const addr of requests) {
1300
+ for (const addr of peerRequests) {
1083
1301
  let ibGib = srcGraph[addr];
1084
1302
  if (!ibGib) {
1085
1303
  const res = await getFromSpace({ addr, space });
@@ -1089,19 +1307,116 @@ export class SyncSagaCoordinator {
1089
1307
  }
1090
1308
  if (ibGib) {
1091
1309
  outgoingPayload.push(ibGib);
1310
+ } else {
1311
+ console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
1312
+ }
1313
+ }
1314
+
1315
+ // 3. Execute Merges (If applicable)
1316
+ // Check if we have pending conflicts that we CAN resolve now that we have data.
1317
+ // We look at the Saga History (Ack Frame) to find conflicts.
1318
+ // Optimization: Do this only if we received payloads.
1319
+ const mergeResultIbGibs: IbGib_V1[] = [];
1320
+
1321
+ if (receivedPayloadIbGibs.length > 0) {
1322
+ // Find the Ack frame in history to get conflicts
1323
+ // Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
1324
+ // V1 timelines carry full history in `past`.
1325
+ const pastAddrs = sagaIbGib.rel8ns?.past || [];
1326
+ let ackData: SyncSagaMessageAckData_V1 | undefined;
1327
+
1328
+ if (pastAddrs.length > 0) {
1329
+ // Batch fetch all past frames
1330
+ const resPast = await getFromSpace({ addrs: pastAddrs, space });
1331
+ if (resPast.success && resPast.ibGibs) {
1332
+ // Iterate backwards (most recent first) to find the latest Ack
1333
+ for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
1334
+ const pastFrame = resPast.ibGibs[i];
1335
+ const messageStone = await getSyncSagaMessageFromFrame({
1336
+ frameIbGib: pastFrame,
1337
+ space
1338
+ });
1339
+ if (messageStone?.data?.stage === SyncStage.ack) {
1340
+ ackData = messageStone.data as SyncSagaMessageAckData_V1;
1341
+ break;
1342
+ }
1343
+ }
1344
+ }
1345
+ }
1346
+
1347
+ if (ackData && ackData.conflicts) {
1348
+ const optimisticConflicts = ackData.conflicts.filter(c => !c.terminal);
1349
+ for (const conflict of optimisticConflicts) {
1350
+ const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
1351
+ // We are Sender (usually) here if we are merging.
1352
+ // Check if we have the history needed (timelineAddrs).
1353
+ // Specifically, we needed the `receiverOnly` parts.
1354
+
1355
+ // We blindly attempt merge if we have both tips accessible?
1356
+ // We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
1357
+
1358
+ // Check if we have receiverTip in space
1359
+ const resRecTip = await getFromSpace({ addr: receiverTip, space });
1360
+ if (resRecTip.success && resRecTip.ibGibs?.[0]) {
1361
+ // We have the tip!
1362
+ // Do we have the full history?
1363
+ // `mergeDivergentTimelines` in `conflict-optimistic` will attempt to fetch history.
1364
+ // If we just ingested the missing pieces, `getFromSpace` inside `merge` should succeed.
1365
+
1366
+ // Perform Merge!
1367
+ try {
1368
+ const mergeResult = await mergeDivergentTimelines({
1369
+ tipA: (await getFromSpace({ addr: senderTip, space })).ibGibs![0], // Our tip
1370
+ tipB: resRecTip.ibGibs[0], // Their tip
1371
+ space,
1372
+ metaspace,
1373
+ });
1374
+ if (mergeResult) {
1375
+ if (logalot) { console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`); }
1376
+ mergeResultIbGibs.push(mergeResult);
1377
+ outgoingPayload.push(mergeResult); // Send result to peer
1378
+ }
1379
+ } catch (e) {
1380
+ console.error(`${lc} Merge failed: ${e}`);
1381
+ // If merge fails, we might Abort or just continue?
1382
+ }
1383
+ }
1384
+ }
1092
1385
  }
1093
1386
  }
1094
1387
 
1095
- // 3. Determine Next Stage
1096
- if (requests.length > 0) {
1097
- // They requested more data -> Send Delta
1098
- const sagaId = deltaData.sagaId;
1388
+ // 4. Determine Next Action
1389
+ // We have `outgoingPayload` (Requests + Merge Results).
1390
+ // Does Peer have outstanding requests? No, we fulfilled `peerRequests`.
1391
+ // Do WE have outstanding requests?
1392
+ // We might if `mergeResult` requires further sync? Usually no, result is complete.
1393
+
1394
+ const myRequests: string[] = []; // If we had more needs (e.g. partial payload), we'd add here.
1395
+
1396
+ const hasOutgoing = outgoingPayload.length > 0;
1397
+ const hasMyRequests = myRequests.length > 0;
1398
+
1399
+ if (hasOutgoing || hasMyRequests) {
1400
+ // We have business to attend to -> Send Delta
1099
1401
  const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
1100
- sagaId,
1402
+ sagaId: deltaData.sagaId,
1101
1403
  stage: SyncStage.delta,
1102
1404
  payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
1405
+ requests: hasMyRequests ? myRequests : undefined,
1406
+ proposeCommit: !hasMyRequests // If we are sending data but have no requests, we VALIDATE PROPOSAL?
1407
+ // Wait. If we send data, we are NOT committing yet.
1408
+ // We are sending data. The OTHER side must ingest it.
1409
+ // So proposeCommit = true?
1410
+ // "Here is the data. I'm done. If you are good, let's commit."
1411
+ // Yes.
1103
1412
  };
1104
1413
 
1414
+ // BUT if `peerProposesCommit` was true, and we are sending data, we are effectively rejecting/delaying it.
1415
+ // We just send the Delta. Peer receives it, ingests, sees ProposeCommit=True (from us), and then Commits.
1416
+
1417
+ // So yes, proposeCommit = true.
1418
+ responseDeltaData.proposeCommit = true;
1419
+
1105
1420
  const deltaStone = await this.createSyncMsgStone({
1106
1421
  data: responseDeltaData,
1107
1422
  space,
@@ -1119,49 +1434,126 @@ export class SyncSagaCoordinator {
1119
1434
  return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
1120
1435
 
1121
1436
  } else {
1122
- // No requests -> Commit
1123
- const sagaId = deltaData.sagaId;
1124
- const commitData: SyncSagaMessageCommitData_V1 = {
1125
- sagaId,
1126
- stage: SyncStage.commit,
1127
- success: true,
1128
- };
1437
+ // We have nothing to send.
1438
+
1439
+ if (peerProposesCommit) {
1440
+ // Peer is done. We are done. -> Commit.
1441
+ const commitData: SyncSagaMessageCommitData_V1 = {
1442
+ sagaId: deltaData.sagaId,
1443
+ stage: SyncStage.commit,
1444
+ success: true,
1445
+ };
1129
1446
 
1130
- const commitStone = await this.createSyncMsgStone({
1131
- data: commitData,
1132
- space,
1133
- metaspace
1134
- });
1447
+ const commitStone = await this.createSyncMsgStone({
1448
+ data: commitData,
1449
+ space,
1450
+ metaspace
1451
+ });
1135
1452
 
1136
- const commitFrame = await this.evolveSyncSagaIbGib({
1137
- prevSagaIbGib: sagaIbGib,
1138
- msgStones: [commitStone],
1139
- identity,
1140
- space,
1141
- metaspace
1142
- });
1453
+ const commitFrame = await this.evolveSyncSagaIbGib({
1454
+ prevSagaIbGib: sagaIbGib,
1455
+ msgStones: [commitStone],
1456
+ identity,
1457
+ space,
1458
+ metaspace
1459
+ });
1460
+
1461
+ return { frame: commitFrame, receivedPayloadIbGibs };
1462
+
1463
+ } else {
1464
+ // peer did NOT propose commit (maybe they just sent data/requests and didn't ready flag).
1465
+ // But we are empty.
1466
+ // So WE propose commit.
1467
+ const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
1468
+ sagaId: deltaData.sagaId,
1469
+ stage: SyncStage.delta,
1470
+ proposeCommit: true,
1471
+ payloadAddrs: [], // Always include empty array if sending delta
1472
+ };
1143
1473
 
1144
- return { frame: commitFrame, receivedPayloadIbGibs };
1474
+ const deltaStone = await this.createSyncMsgStone({
1475
+ data: responseDeltaData,
1476
+ space,
1477
+ metaspace
1478
+ });
1479
+
1480
+ const deltaFrame = await this.evolveSyncSagaIbGib({
1481
+ prevSagaIbGib: sagaIbGib,
1482
+ msgStones: [deltaStone],
1483
+ identity,
1484
+ space,
1485
+ metaspace
1486
+ });
1487
+
1488
+ // Check if PEER proposed commit
1489
+ if (deltaData.proposeCommit) {
1490
+ if (logalot) { console.log(`${lc} Peer proposed commit. Accepting & Committing.`); }
1491
+ // Peer wants to commit and has no more requests.
1492
+ // We should Commit.
1493
+
1494
+ const commitData: SyncSagaMessageCommitData_V1 = {
1495
+ sagaId: deltaData.sagaId,
1496
+ stage: SyncStage.commit,
1497
+ success: true,
1498
+ };
1499
+
1500
+ const commitStone = await this.createSyncMsgStone({
1501
+ data: commitData,
1502
+ space,
1503
+ metaspace
1504
+ });
1505
+
1506
+ const commitFrame = await this.evolveSyncSagaIbGib({
1507
+ prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
1508
+ msgStones: [commitStone],
1509
+ identity,
1510
+ space,
1511
+ metaspace
1512
+ });
1513
+
1514
+ return { frame: commitFrame, receivedPayloadIbGibs };
1515
+ }
1516
+
1517
+ return { frame: deltaFrame, receivedPayloadIbGibs };
1518
+ }
1145
1519
  }
1146
1520
  }
1147
1521
 
1522
+
1148
1523
  protected async handleCommitFrame({
1149
1524
  sagaIbGib,
1150
1525
  space,
1526
+ metaspace,
1151
1527
  }: {
1152
1528
  sagaIbGib: SyncIbGib_V1,
1153
1529
  space: IbGibSpaceAny,
1530
+ metaspace: MetaspaceService,
1531
+ identity?: KeystoneIbGib_V1,
1154
1532
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
1155
1533
  const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
1156
- if (logalot) { console.log(`${lc} Commit received. Saga complete.`); }
1534
+ if (logalot) { console.log(`${lc} Commit received.`); }
1535
+
1536
+ // Sender Logic (Finalizing):
1537
+ // If we are here, we received a Commit frame from the Peer.
1538
+ // This implies the Peer has successfully committed.
1539
+ // We should now:
1540
+ // 1. Validate (implicitly done by receiving valid frame)
1541
+ // 2. Perform our own cleanup (Temp -> Dest, if applicable)
1542
+ // 3. Return null to signal saga completion.
1543
+
1544
+ // Note: Currently we don't have explicit cleanup logic implemented here yet (TODO).
1545
+
1546
+ if (logalot) { console.log(`${lc} Peer committed. Finalizing saga locally. Saga Complete.`); }
1157
1547
  return null;
1158
1548
  }
1159
1549
 
1160
1550
  protected async handleConflictFrame({
1161
1551
  sagaIbGib,
1552
+ metaspace,
1162
1553
  space,
1163
1554
  }: {
1164
1555
  sagaIbGib: SyncIbGib_V1,
1556
+ metaspace: MetaspaceService,
1165
1557
  space: IbGibSpaceAny,
1166
1558
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
1167
1559
  const lc = `${this.lc}[${this.handleConflictFrame.name}]`;
@@ -1189,16 +1581,27 @@ export class SyncSagaCoordinator {
1189
1581
  space: IbGibSpaceAny,
1190
1582
  metaspace: MetaspaceService,
1191
1583
  }): Promise<IbGib_V1<TStoneData>> {
1192
- const ib = await getSyncSagaMessageIb({ data });
1193
- const stone = await Factory_V1.stone({
1194
- ib,
1195
- parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
1196
- data,
1197
- uuid: true, // we want the stone to have its own uniqueness
1198
- });
1199
- await putInSpace({ space, ibGib: stone });
1200
- await metaspace.registerNewIbGib({ ibGib: stone });
1201
- return stone as IbGib_V1<TStoneData>;
1584
+ const lc = `${this.lc}[${this.createSyncMsgStone.name}]`;
1585
+ try {
1586
+ if (logalot) { console.log(`${lc} starting... (I: 5f7f98e8ff980364f7191fcee4531e26)`); }
1587
+
1588
+ const ib = await getSyncSagaMessageIb({ data });
1589
+ const stone = await Factory_V1.stone({
1590
+ ib,
1591
+ parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
1592
+ data,
1593
+ uuid: true, // we want the stone to have its own uniqueness
1594
+ });
1595
+ if (logalot) { console.log(`${lc} Created stone: ${getIbGibAddr({ ibGib: stone })}`); }
1596
+ await putInSpace({ space, ibGib: stone });
1597
+ await metaspace.registerNewIbGib({ ibGib: stone });
1598
+ return stone as IbGib_V1<TStoneData>;
1599
+ } catch (error) {
1600
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1601
+ throw error;
1602
+ } finally {
1603
+ if (logalot) { console.log(`${lc} complete.`); }
1604
+ }
1202
1605
  }
1203
1606
 
1204
1607
 
@@ -1211,12 +1614,14 @@ export class SyncSagaCoordinator {
1211
1614
  identity,
1212
1615
  space,
1213
1616
  metaspace,
1617
+ conflictStrategy,
1214
1618
  }: {
1215
1619
  prevSagaIbGib?: SyncIbGib_V1,
1216
1620
  msgStones: IbGib_V1[],
1217
1621
  identity?: KeystoneIbGib_V1,
1218
1622
  space: IbGibSpaceAny,
1219
1623
  metaspace: MetaspaceService,
1624
+ conflictStrategy?: SyncConflictStrategy,
1220
1625
  }): Promise<SyncIbGib_V1> {
1221
1626
  const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
1222
1627
  try {
@@ -1293,6 +1698,7 @@ export class SyncSagaCoordinator {
1293
1698
  payload: undefined, // Data in stone
1294
1699
  n: 0,
1295
1700
  isTjp: true,
1701
+ conflictStrategy,
1296
1702
  };
1297
1703
  const ib = await getSyncIb({ data });
1298
1704