@ibgib/core-gib 0.1.19 → 0.1.21

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 (81) 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/graft-info/graft-info-constants.d.mts +5 -0
  6. package/dist/sync/graft-info/graft-info-constants.d.mts.map +1 -0
  7. package/dist/sync/graft-info/graft-info-constants.mjs +5 -0
  8. package/dist/sync/graft-info/graft-info-constants.mjs.map +1 -0
  9. package/dist/sync/graft-info/graft-info-helpers.d.mts +49 -0
  10. package/dist/sync/graft-info/graft-info-helpers.d.mts.map +1 -0
  11. package/dist/sync/graft-info/graft-info-helpers.mjs +236 -0
  12. package/dist/sync/graft-info/graft-info-helpers.mjs.map +1 -0
  13. package/dist/sync/graft-info/graft-info-helpers.respec.d.mts +2 -0
  14. package/dist/sync/graft-info/graft-info-helpers.respec.d.mts.map +1 -0
  15. package/dist/sync/graft-info/graft-info-helpers.respec.mjs +70 -0
  16. package/dist/sync/graft-info/graft-info-helpers.respec.mjs.map +1 -0
  17. package/dist/sync/graft-info/graft-info-types.d.mts +31 -0
  18. package/dist/sync/graft-info/graft-info-types.d.mts.map +1 -0
  19. package/dist/sync/graft-info/graft-info-types.mjs +2 -0
  20. package/dist/sync/graft-info/graft-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 +112 -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 +277 -0
  28. package/dist/sync/sync-conflict.respec.mjs.map +1 -0
  29. package/dist/sync/sync-constants.d.mts +1 -3
  30. package/dist/sync/sync-constants.d.mts.map +1 -1
  31. package/dist/sync/sync-constants.mjs +0 -2
  32. package/dist/sync/sync-constants.mjs.map +1 -1
  33. package/dist/sync/sync-innerspace-constants.respec.mjs +2 -2
  34. package/dist/sync/sync-innerspace-constants.respec.mjs.map +1 -1
  35. package/dist/sync/sync-innerspace-deep-updates.respec.mjs +0 -1
  36. package/dist/sync/sync-innerspace-deep-updates.respec.mjs.map +1 -1
  37. package/dist/sync/sync-innerspace.respec.mjs +1 -1
  38. package/dist/sync/sync-innerspace.respec.mjs.map +1 -1
  39. package/dist/sync/sync-peer/sync-peer-innerspace-v1.d.mts +5 -2
  40. package/dist/sync/sync-peer/sync-peer-innerspace-v1.d.mts.map +1 -1
  41. package/dist/sync/sync-peer/sync-peer-innerspace-v1.mjs +70 -7
  42. package/dist/sync/sync-peer/sync-peer-innerspace-v1.mjs.map +1 -1
  43. package/dist/sync/sync-saga-coordinator.d.mts +45 -27
  44. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  45. package/dist/sync/sync-saga-coordinator.mjs +811 -253
  46. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  47. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts +11 -0
  48. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts.map +1 -1
  49. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs +25 -0
  50. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs.map +1 -1
  51. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +24 -12
  52. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -1
  53. package/dist/sync/sync-types.d.mts +31 -4
  54. package/dist/sync/sync-types.d.mts.map +1 -1
  55. package/dist/sync/sync-types.mjs.map +1 -1
  56. package/ibgib-foundations.md +147 -0
  57. package/package.json +1 -1
  58. package/roadmap.md +59 -0
  59. package/src/common/other/ibgib-helper.mts +52 -0
  60. package/src/keystone/README.md +13 -155
  61. package/src/keystone/docs/architecture.md +55 -0
  62. package/src/sync/README.md +37 -42
  63. package/src/sync/docs/architecture.md +69 -0
  64. package/src/sync/docs/verification.md +43 -0
  65. package/src/sync/graft-info/graft-info-constants.mts +4 -0
  66. package/src/sync/graft-info/graft-info-helpers.mts +308 -0
  67. package/src/sync/graft-info/graft-info-helpers.respec.mts +83 -0
  68. package/src/sync/graft-info/graft-info-types.mts +33 -0
  69. package/src/sync/strategies/conflict-optimistic.mts +149 -0
  70. package/src/sync/sync-conflict.respec.mts +330 -0
  71. package/src/sync/sync-constants.mts +1 -4
  72. package/src/sync/sync-innerspace-constants.respec.mts +1 -1
  73. package/src/sync/sync-innerspace-deep-updates.respec.mts +0 -1
  74. package/src/sync/sync-innerspace.respec.mts +1 -1
  75. package/src/sync/sync-peer/sync-peer-innerspace-v1.mts +85 -12
  76. package/src/sync/sync-saga-coordinator.mts +905 -268
  77. package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +43 -0
  78. package/src/sync/sync-saga-message/sync-saga-message-types.mts +23 -11
  79. package/src/sync/sync-types.mts +33 -4
  80. package/test_output.log +0 -0
  81. package/tmp.md +44 -426
@@ -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";
@@ -30,7 +31,7 @@ import { getDependencyGraph } from "../common/other/graph-helper.mjs";
30
31
  import {
31
32
  SyncSagaMessageData_V1, SyncSagaMessageInitData_V1,
32
33
  SyncSagaMessageAckData_V1, SyncSagaMessageDeltaData_V1,
33
- SyncSagaMessageCommitData_V1, SyncSagaMessageConflictData_V1
34
+ SyncSagaMessageCommitData_V1
34
35
  } from "./sync-saga-message/sync-saga-message-types.mjs";
35
36
  import { getSyncSagaMessageIb } from "./sync-saga-message/sync-saga-message-helpers.mjs";
36
37
  import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-constants.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
 
@@ -140,7 +143,7 @@ export class SyncSagaCoordinator {
140
143
  const sessionIdentity = useSessionIdentity
141
144
  ? await this.getSessionIdentity({ sagaId, metaspace, tempSpace })
142
145
  : undefined;
143
- if (logalot) { console.log(`${lc} sessionIdentity: ${sessionIdentity ? pretty(sessionIdentity) : 'undefined'} (I: abc01872800b3a66b819a05898bba826)`); }
146
+ // if (logalot) { console.log(`${lc} sessionIdentity: ${sessionIdentity ? pretty(sessionIdentity) : 'undefined'} (I: abc01872800b3a66b819a05898bba826)`); }
144
147
 
145
148
  // 3. CREATE INITIAL FRAME (Stage.init)
146
149
  const { sagaFrame: initFrame, srcGraph } = await this.createInitFrame({
@@ -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)
@@ -331,12 +335,23 @@ export class SyncSagaCoordinator {
331
335
  }
332
336
 
333
337
  // B. Transmit
334
- if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
338
+ // if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
335
339
  updates$.next(requestCtx);
336
340
  const responseCtx = await peer.witness(requestCtx);
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;
@@ -374,6 +389,7 @@ export class SyncSagaCoordinator {
374
389
  await putInSpace({ space: tempSpace, ibGibs: remoteDeps });
375
390
  }
376
391
 
392
+
377
393
  // React (Reducer)
378
394
  // This processes the FRAME we just got from the peer.
379
395
  // i.e., We Sent Init -> Got Ack. This calls handleAckFrame.
@@ -381,7 +397,8 @@ export class SyncSagaCoordinator {
381
397
  const result = await this.handleSagaFrame({
382
398
  sagaIbGib: remoteFrame as SyncIbGib_V1,
383
399
  srcGraph,
384
- space: localSpace, // Must be localSpace (Source) to find domain data
400
+ destSpace: localSpace, // Query existing data from localSpace (Source)
401
+ tempSpace: tempSpace, // Transaction space for saga frames
385
402
  identity: sessionIdentity,
386
403
  metaspace
387
404
  });
@@ -397,6 +414,83 @@ export class SyncSagaCoordinator {
397
414
  return allReceivedIbGibs;
398
415
  }
399
416
 
417
+ /**
418
+ * Helper to get Knowledge Vector for specific domain ibGibs or TJPs.
419
+ * Useful for testing and external validation.
420
+ */
421
+ public async getKnowledgeVector({
422
+ space,
423
+ metaspace,
424
+ domainIbGibs,
425
+ tjpAddrs,
426
+ }: {
427
+ space: IbGibSpaceAny,
428
+ metaspace: MetaspaceService,
429
+ domainIbGibs?: IbGib_V1[],
430
+ tjpAddrs?: string[],
431
+ }): Promise<{ [tjp: string]: string | null }> {
432
+ const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
433
+ try {
434
+ if (logalot) { console.log(`${lc} starting... (I: e184f8a7818666febfbbd2d841ed3826)`); }
435
+ console.dir(space)
436
+
437
+ if (!(domainIbGibs && domainIbGibs.length > 0) &&
438
+ !(tjpAddrs && tjpAddrs.length > 0)
439
+ ) {
440
+ throw new Error(`(UNEXPECTED) domainIbGibs and tjpAddrs falsy/empty? we need one or the other (E: f674285111c8648398cd79d8c08ec826)`);
441
+ }
442
+
443
+ if ((domainIbGibs && domainIbGibs.length > 0) &&
444
+ (tjpAddrs && tjpAddrs.length > 0)
445
+ ) {
446
+ throw new Error(`(UNEXPECTED) both domainIbGibs and tjpAddrs truthy? only pass in one or the other. (E: f674285111c8648398cd79d8c08ec826)`);
447
+ }
448
+
449
+ let tjps: string[] = [];
450
+ if (tjpAddrs) {
451
+ tjps = tjpAddrs;
452
+ } else if (domainIbGibs && domainIbGibs.length > 0) {
453
+ // Extract TJPs from domain Ibgibs
454
+ if (logalot) { console.log(`${lc} domainIbGibs (${domainIbGibs.length}) provided. (I: a378995a0658af1f086ac1f297486c26)`); }
455
+
456
+ const { mapWithTjp_YesDna, mapWithTjp_NoDna } =
457
+ splitPerTjpAndOrDna({ ibGibs: domainIbGibs });
458
+ if (logalot) { console.log(`${lc}[TEST DEBUG] mapWithTjp_YesDna: ${JSON.stringify(mapWithTjp_YesDna)} (I: 287e22897148298e185712c8d50cfb26)`); }
459
+ if (logalot) { console.log(`${lc}[TEST DEBUG] mapWithTjp_NoDna: ${JSON.stringify(mapWithTjp_NoDna)} (I: 1bdc62656294aed0f9df334647dc7326)`); }
460
+
461
+ const allWithTjp = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
462
+ const timelineMap = getTimelinesGroupedByTjp({ ibGibs: allWithTjp });
463
+ if (logalot) { console.log(`${lc}[TEST DEBUG] timelineMap: ${JSON.stringify(timelineMap)} (I: 2cc04898e5f85179fb1ac7f827abc426)`); }
464
+
465
+ tjps = Object.keys(timelineMap);
466
+ if (logalot) { console.log(`${lc}[TEST DEBUG] tjps: ${tjps} (I: 3dd548667cbd967c68e57c88dc570826)`); }
467
+ } else {
468
+ // No info provided. Return empty? Or throw?
469
+ // User test context implied "everything", but implementation requires scope.
470
+ console.warn(`${lc} No domainIbGibs or tjpAddrs provided. Returning empty KV.`);
471
+ return {};
472
+ }
473
+
474
+ if (tjps.length === 0) { return {}; }
475
+
476
+ if (logalot) { console.log(`${lc} getting latest addrs for tjps: ${tjps} (I: d4e7080b8ba8187c583b82fd91ac0626)`); }
477
+
478
+ const res = await getLatestAddrs({ space, tjpAddrs: tjps });
479
+ if (!res.data || !res.data.latestAddrsMap) {
480
+ throw new Error(`${lc} Failed to get latest addrs. (E: 7a8b9c0d)`);
481
+ }
482
+
483
+ if (logalot) { console.log(`${lc}[TEST DEBUG] res.data.latestAddrsMap: ${JSON.stringify(res.data.latestAddrsMap)} (I: a8e128bdf80898ac2e6d8021a5bff726)`); }
484
+
485
+ return res.data.latestAddrsMap;
486
+ } catch (error) {
487
+ console.error(`${lc} ${extractErrorMsg(error)}`);
488
+ throw error;
489
+ } finally {
490
+ if (logalot) { console.log(`${lc} complete.`); }
491
+ }
492
+ }
493
+
400
494
  protected async analyzeTimelines({
401
495
  domainIbGibs,
402
496
  space,
@@ -450,14 +544,16 @@ export class SyncSagaCoordinator {
450
544
  localSpace,
451
545
  domainIbGibs,
452
546
  tempSpace,
453
- metaspace
547
+ metaspace,
548
+ conflictStrategy,
454
549
  }: {
455
550
  sagaId: string,
456
551
  sessionIdentity?: KeystoneIbGib_V1,
457
552
  localSpace: IbGibSpaceAny,
458
553
  domainIbGibs: IbGib_V1[],
459
554
  tempSpace: IbGibSpaceAny,
460
- metaspace: MetaspaceService
555
+ metaspace: MetaspaceService,
556
+ conflictStrategy: SyncConflictStrategy,
461
557
  }): Promise<{ sagaFrame: SyncIbGib_V1, srcGraph: { [addr: string]: IbGib_V1 } }> {
462
558
  const lc = `${this.lc}[${this.createInitFrame.name}]`;
463
559
  try {
@@ -485,20 +581,35 @@ export class SyncSagaCoordinator {
485
581
  initData.knowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
486
582
  });
487
583
 
584
+ if (logalot) {
585
+ console.log(`${lc} SyncStage.init: ${SyncStage.init}, SyncStage.commit: ${SyncStage.commit}`);
586
+ console.log(`${lc} initData.stage: ${initData.stage}`);
587
+ }
588
+
488
589
  const initStone = await this.createSyncMsgStone({
489
590
  data: initData,
490
591
  space: tempSpace,
491
592
  metaspace
492
593
  });
493
- if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
594
+ // if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
494
595
 
495
596
  const sagaFrame = await this.evolveSyncSagaIbGib({
496
597
  msgStones: [initStone],
497
598
  identity: sessionIdentity,
498
599
  space: tempSpace,
600
+ metaspace,
601
+ conflictStrategy,
602
+ });
603
+
604
+ // IMMEDIATELY persist to both spaces for audit trail
605
+ await this.ensureSagaFrameInBothSpaces({
606
+ frame: sagaFrame,
607
+ destSpace: localSpace, // localSpace is the Sender's destSpace
608
+ tempSpace,
499
609
  metaspace
500
610
  });
501
- if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
611
+
612
+ // if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
502
613
 
503
614
  return { sagaFrame, srcGraph };
504
615
 
@@ -526,14 +637,16 @@ export class SyncSagaCoordinator {
526
637
  async handleSagaFrame({
527
638
  sagaIbGib,
528
639
  srcGraph,
529
- space,
640
+ destSpace,
641
+ tempSpace,
530
642
  identity,
531
643
  identitySecret,
532
644
  metaspace,
533
645
  }: {
534
646
  sagaIbGib: SyncIbGib_V1,
535
647
  srcGraph: { [addr: string]: IbGib_V1 },
536
- space: IbGibSpaceAny,
648
+ destSpace: IbGibSpaceAny,
649
+ tempSpace: IbGibSpaceAny,
537
650
  identity?: KeystoneIbGib_V1,
538
651
  identitySecret?: string,
539
652
  metaspace: MetaspaceService,
@@ -546,25 +659,22 @@ export class SyncSagaCoordinator {
546
659
  if (logalot) { console.log(`${lc} sagaIbGib: ${pretty(sagaIbGib)} (I: 1b99d87d262e9d18d8a607a80b1a0126)`); }
547
660
 
548
661
  // Get Stage from Stone (or Frame for Init fallback)
549
- const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
662
+ const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
550
663
 
551
664
  if (logalot) { console.log(`${lc} handling frame stage: ${stage}`); }
552
665
 
553
666
  switch (stage) {
554
667
  case SyncStage.init:
555
- return await this.handleInitFrame({ sagaIbGib, messageData, space, metaspace, identity, identitySecret });
668
+ return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, destSpace, tempSpace, identity, identitySecret });
556
669
 
557
670
  case SyncStage.ack:
558
- return await this.handleAckFrame({ sagaIbGib, srcGraph, space, metaspace, identity });
671
+ return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity });
559
672
 
560
673
  case SyncStage.delta:
561
- return await this.handleDeltaFrame({ sagaIbGib, srcGraph, space, identity, metaspace });
674
+ return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity, });
562
675
 
563
676
  case SyncStage.commit:
564
- return await this.handleCommitFrame({ sagaIbGib, space });
565
-
566
- case SyncStage.conflict:
567
- return await this.handleConflictFrame({ sagaIbGib, space });
677
+ return await this.handleCommitFrame({ sagaIbGib, metaspace, destSpace, tempSpace, identity, });
568
678
 
569
679
  default:
570
680
  throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
@@ -594,31 +704,39 @@ export class SyncSagaCoordinator {
594
704
  protected async handleInitFrame({
595
705
  sagaIbGib,
596
706
  messageData,
597
- space,
707
+ destSpace,
708
+ tempSpace,
598
709
  metaspace,
599
710
  identity,
600
711
  identitySecret,
601
712
  }: {
602
713
  sagaIbGib: SyncIbGib_V1,
603
714
  messageData: any,
604
- space: IbGibSpaceAny,
715
+ destSpace: IbGibSpaceAny,
716
+ tempSpace: IbGibSpaceAny,
605
717
  metaspace: MetaspaceService,
606
718
  identity?: KeystoneIbGib_V1,
607
719
  identitySecret?: string,
608
720
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
609
721
  const lc = `${this.lc}[${this.handleInitFrame.name}]`;
722
+ console.log(`${lc} [TEST DEBUG] Received destSpace: ${destSpace.data?.name || destSpace.ib} (uuid: ${destSpace.data?.uuid || '[no uuid]'})`);
610
723
  if (logalot) { console.log(`${lc} starting...`); }
611
724
 
612
725
  // Extract Init Data
613
726
  const initData = messageData as SyncSagaMessageInitData_V1; // Using renamed variable for clarity
614
- if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
727
+ if (initData.stage !== SyncStage.init) {
728
+ throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
729
+ }
730
+ // if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
615
731
  if (!initData || !initData.knowledgeVector) {
616
732
  throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
617
733
  }
618
734
 
735
+ // Determine Strategy from Saga Data (since V1 stores it in root)
736
+ const conflictStrategy = sagaIbGib.data!.conflictStrategy || 'abort';
737
+
619
738
  // 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
739
+ const conflicts: { tjpAddr: string, localAddr: string, remoteAddr: string, timelineAddrs: string[], reason: string, terminal: boolean }[] = [];
622
740
 
623
741
  const deltaReqAddrs: string[] = [];
624
742
  const pushOfferAddrs: string[] = [];
@@ -628,7 +746,7 @@ export class SyncSagaCoordinator {
628
746
  if (stones.length > 0) {
629
747
  if (logalot) { console.log(`${lc} processing stones: ${stones.length}`); }
630
748
  // Check if we have these stones
631
- const resStones = await getFromSpace({ space, addrs: stones });
749
+ const resStones = await getFromSpace({ space: destSpace, addrs: stones });
632
750
  const addrsNotFound = resStones.rawResultIbGib?.data?.addrsNotFound;
633
751
  if (addrsNotFound && addrsNotFound.length > 0) {
634
752
  if (logalot) { console.log(`${lc} stones missing (requesting): ${addrsNotFound.length}`); }
@@ -643,6 +761,7 @@ export class SyncSagaCoordinator {
643
761
  const remoteKV = initData.knowledgeVector;
644
762
  if (logalot) { console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`); }
645
763
  const remoteTjps = Object.keys(remoteKV);
764
+ console.log(`${lc} [TEST DEBUG] remoteTjps: ${JSON.stringify(remoteTjps)}`);
646
765
  if (logalot) { console.log(`${lc} remoteTjps: ${pretty(remoteTjps)} (I: 86ea4c53db0dc184c8b253386c402126)`); }
647
766
 
648
767
  // 1. Get Local Latest Addrs for all TJPs
@@ -650,12 +769,13 @@ export class SyncSagaCoordinator {
650
769
  if (remoteTjps.length > 0) {
651
770
  // Batch get latest addrs for the TJPs
652
771
  const resGetLatestAddrs = await getLatestAddrs({
653
- space,
772
+ space: destSpace,
654
773
  tjpAddrs: remoteTjps,
655
774
  });
656
775
  if (!resGetLatestAddrs.data) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data falsy? (E: b180d813c088042b38e1e02e06a16926)`); }
657
776
  if (!resGetLatestAddrs.data.latestAddrsMap) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data.latestAddrsMap falsy? (E: 16bc386dd51d0ff53a49620b1e641826)`); }
658
777
  localKV = resGetLatestAddrs.data.latestAddrsMap;
778
+ console.log(`${lc} [TEST DEBUG] localKV: ${JSON.stringify(localKV)}`);
659
779
  if (logalot) { console.log(`${lc} localKV: ${pretty(localKV)} (I: 980975642cbccd8018cf0cd808d30826)`); }
660
780
  }
661
781
 
@@ -668,24 +788,28 @@ export class SyncSagaCoordinator {
668
788
 
669
789
  if (!localAddr) {
670
790
  // We (Receiver) don't have this timeline. Request it.
791
+ console.log(`${lc} [TEST DEBUG] Missing local timeline for TJP: ${tjp}. Requesting remoteAddr: ${remoteAddr}`);
671
792
  deltaReqAddrs.push(remoteAddr);
672
793
  continue;
673
794
  }
674
795
 
675
796
  if (localAddr === remoteAddr) {
676
797
  // Synced
798
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Synced (localAddr === remoteAddr)`);
677
799
  continue;
678
800
  }
801
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: localAddr=${localAddr}, remoteAddr=${remoteAddr} - checking for divergence...`);
679
802
 
680
803
  // Check if Remote is in Local's PAST (Local is Ahead -> Push Offer)
681
804
  // (Sender has older version, Receiver has newer) -> Receiver Offers Push
682
805
  const isRemoteInPast = await isPastFrame({
683
806
  olderAddr: remoteAddr,
684
807
  newerAddr: localAddr,
685
- space,
808
+ space: destSpace,
686
809
  });
687
810
 
688
811
  if (isRemoteInPast) {
812
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Remote is in past - offering push`);
689
813
  pushOfferAddrs.push(localAddr);
690
814
  } else {
691
815
  // Remote is not in our past.
@@ -695,29 +819,57 @@ export class SyncSagaCoordinator {
695
819
  const isLocalInPast = await isPastFrame({
696
820
  olderAddr: localAddr,
697
821
  newerAddr: remoteAddr,
698
- space,
822
+ space: destSpace,
699
823
  });
700
824
 
701
825
  if (isLocalInPast) {
702
826
  // Fast-Forward: We update to remote's tip.
827
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Local is in past - requesting delta`);
703
828
  deltaReqAddrs.push(remoteAddr);
704
829
  } else {
705
830
  // 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
831
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: DIVERGENCE DETECTED! conflictStrategy=${conflictStrategy}`);
712
832
 
713
833
  if (conflictStrategy === 'abort') {
714
- // We will abort immediately after this loop check if any conflicts exist
834
+ // Abort Strategy: We will treat this as terminal.
835
+ // But for Unified Ack, we just mark it terminal in the list?
836
+ // Or do we actually throw/abort the saga?
837
+ // Current logic (below) aborts the saga if ANY conflict is terminal/abort.
838
+ conflicts.push({
839
+ tjpAddr: tjp,
840
+ localAddr: localAddr!,
841
+ remoteAddr,
842
+ timelineAddrs: [], // Not needed for abort
843
+ reason: 'divergence',
844
+ terminal: true
845
+ });
715
846
  } 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);
847
+ // Optimistic: We want to resolving this.
848
+ // We need to send our history to the Sender so they can Merge.
849
+
850
+ // Fetch Full History for Local Timeline
851
+ // Note: We might optimize this to only send "recent" history if we had a KV?
852
+ // But for now, get full past.
853
+ // Optimization: localKV might not have full history.
854
+ // We need to inspect the 'past' of the local tip.
855
+
856
+ // We need the ACTUAL object to get the past.
857
+ // We have localAddr.
858
+ const resLocalTip = await getFromSpace({ space: destSpace, addr: localAddr! });
859
+ const localTip = resLocalTip.ibGibs?.[0];
860
+ if (!localTip) { throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`); }
861
+
862
+ const timelineAddrs = [localAddr!, ...(localTip.rel8ns?.past || [])];
863
+
864
+ conflicts.push({
865
+ tjpAddr: tjp,
866
+ localAddr: localAddr!,
867
+ remoteAddr,
868
+ timelineAddrs,
869
+ reason: 'divergence',
870
+ terminal: false
871
+ });
872
+
721
873
  } else {
722
874
  throw new Error(`${lc} Unsupported conflict strategy: ${conflictStrategy} (E: 2a9b3c4d5e6f7g8h9i0j)`);
723
875
  }
@@ -725,40 +877,32 @@ export class SyncSagaCoordinator {
725
877
  }
726
878
  }
727
879
 
728
- if (conflicts.length > 0 && conflictStrategy === 'abort') {
880
+ // Check if we should ABORT (if any conflict is terminal)
881
+ const hasTerminalConflicts = conflicts.some(c => c.terminal);
882
+
883
+ if (hasTerminalConflicts) {
729
884
  // Abort Strategy: Kill the saga.
730
- if (logalot) { console.warn(`${lc} ABORTING Sync Saga due to conflicts: ${JSON.stringify(conflicts)}`); }
885
+ if (logalot) { console.warn(`${lc} ABORTING Sync Saga due to terminal conflicts: ${JSON.stringify(conflicts)}`); }
731
886
 
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
- };
887
+ // We reuse the ConflictData structure for terminal aborts?
888
+ // Or do we send an Ack with terminal conflicts?
889
+ // Original design had explicit Conflict Frame for Abort.
890
+ // Let's stick to that for purely terminal cases to be safe/explicit?
891
+ // Or Unified: Just send Ack with terminal=true conflicts. Sender sees them and aborts.
740
892
 
741
- const conflictStone = await this.createSyncMsgStone({
742
- data: conflictData,
743
- space,
744
- metaspace,
745
- });
746
-
747
- const conflictFrame = await this.evolveSyncSagaIbGib({
748
- prevSagaIbGib: sagaIbGib,
749
- msgStones: [conflictStone],
750
- identity,
751
- space,
752
- metaspace,
753
- });
893
+ // Decision: Unified Ack for everything is cleaner protocol.
894
+ // But wait, the original code below creates a Conflict Stone.
895
+ // Let's preserve the explicit 'Conflict' frame for total aborts if that's easier,
896
+ // OR fully switch to Ack.
897
+ // Protocol states: Init -> Ack. If Ack contains terminal errors, Sender can Commit(Fail).
754
898
 
755
- return { frame: conflictFrame };
899
+ // Let's use Ack with conflicts.
756
900
  }
757
901
 
758
902
  // 2. Add Push Offers (Missing in Local)
759
903
  // Check if we have them. If not, ask for them.
760
904
  for (const addr of pushOfferAddrs) {
761
- const hasIt = await getFromSpace({ addr, space });
905
+ const hasIt = await getFromSpace({ addr, space: destSpace });
762
906
  if (!hasIt.success || !hasIt.ibGibs || hasIt.ibGibs.length === 0) {
763
907
  // If we don't have it, we put it in `deltaReqAddrs` of the Ack.
764
908
  deltaReqAddrs.push(addr);
@@ -777,7 +921,7 @@ export class SyncSagaCoordinator {
777
921
  for (const tjp of remoteTjps) {
778
922
  const localAddr = localKV[tjp];
779
923
  if (localAddr) {
780
- const res = await getFromSpace({ addr: localAddr, space });
924
+ const res = await getFromSpace({ addr: localAddr, space: destSpace });
781
925
  if (res.success && res.ibGibs?.[0]) {
782
926
  const ibGib = res.ibGibs[0];
783
927
  const realTjp = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib }); // Should match `tjp` if normalized
@@ -795,7 +939,7 @@ export class SyncSagaCoordinator {
795
939
  // We can't really know if it's covered easily without resolving.
796
940
  // But if we don't have it (requesting), we won't find it here anyway.
797
941
  // If we DO have it (push offer), we might find it.
798
- const res = await getFromSpace({ addr, space });
942
+ const res = await getFromSpace({ addr, space: destSpace });
799
943
  if (res.success && res.ibGibs?.[0]) {
800
944
  const ibGib = res.ibGibs[0];
801
945
  const tjpAddr = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib });
@@ -817,27 +961,7 @@ export class SyncSagaCoordinator {
817
961
  // Problem: Receiver doesn't know X is related to Y without X.
818
962
 
819
963
  // 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`?
964
+ // The *Sender* must include TJP mappings or we rely on `knowledgeVector` in `Init`?
841
965
  // `SyncSagaMessageInitData_V1` extends `SyncInitData`.
842
966
  // `SyncInitData` has `knowledgeVector`.
843
967
  // If Sender populates `knowledgeVector` in `Init`, Receiver can use keys (TJPs) to look up its own state!
@@ -854,11 +978,12 @@ export class SyncSagaCoordinator {
854
978
  deltaReqAddrs,
855
979
  pushOfferAddrs,
856
980
  knowledgeVector,
981
+ conflicts: conflicts.length > 0 ? conflicts : undefined, // Include conflicts if any detected
857
982
  };
858
983
 
859
984
  const ackStone = await this.createSyncMsgStone({
860
985
  data: ackData,
861
- space,
986
+ space: tempSpace,
862
987
  metaspace,
863
988
  });
864
989
  if (logalot) { console.log(`${lc} ackStone created: ${pretty(ackStone)} (I: 313708132dd53ff946befb7833657826)`); }
@@ -868,11 +993,14 @@ export class SyncSagaCoordinator {
868
993
  prevSagaIbGib: sagaIbGib,
869
994
  msgStones: [ackStone],
870
995
  identity,
871
- space,
996
+ space: tempSpace,
872
997
  metaspace,
873
998
  });
874
999
 
875
- if (logalot) { console.log(`${lc} ackFrame created: ${pretty(ackFrame)} (I: be24480592eec478086bb3da49286826)`); }
1000
+ // IMMEDIATELY persist to both spaces for audit trail (before any errors can occur)
1001
+ await this.ensureSagaFrameInBothSpaces({ frame: ackFrame, destSpace, tempSpace, metaspace });
1002
+
1003
+ // if (logalot) { console.log(`${lc} ackFrame created: ${pretty(ackFrame)} (I: be24480592eec478086bb3da49286826)`); }
876
1004
 
877
1005
  return { frame: ackFrame };
878
1006
  }
@@ -892,138 +1020,364 @@ export class SyncSagaCoordinator {
892
1020
  protected async handleAckFrame({
893
1021
  sagaIbGib,
894
1022
  srcGraph,
895
- space,
1023
+ destSpace,
1024
+ tempSpace,
896
1025
  metaspace,
897
1026
  identity,
898
1027
  }: {
899
1028
  sagaIbGib: SyncIbGib_V1,
900
1029
  srcGraph: { [addr: string]: IbGib_V1 },
901
- space: IbGibSpaceAny,
1030
+ destSpace: IbGibSpaceAny,
1031
+ tempSpace: IbGibSpaceAny,
902
1032
  metaspace: MetaspaceService,
903
1033
  identity?: KeystoneIbGib_V1,
904
1034
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
905
1035
  const lc = `${this.lc}[${this.handleAckFrame.name}]`;
906
- if (logalot) { console.log(`${lc} starting...`); }
1036
+ try {
1037
+ if (logalot) { console.log(`${lc} starting... (I: 605b6860e898267a5b50c6d85704be26)`); }
907
1038
 
908
- const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
909
- const ackData = messageData as SyncSagaMessageAckData_V1;
1039
+ const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
1040
+ const ackData = messageData as SyncSagaMessageAckData_V1;
910
1041
 
911
- if (!ackData) {
912
- throw new Error(`${lc} ackData falsy (E: 3b8415edc876084c88a25b98e2d55826)`);
913
- }
914
- if (ackData.stage !== SyncStage.ack) {
915
- throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
916
- }
1042
+ if (!ackData) {
1043
+ throw new Error(`${lc} ackData falsy (E: 3b8415edc876084c88a25b98e2d55826)`);
1044
+ }
1045
+ if (ackData.stage !== SyncStage.ack) {
1046
+ throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
1047
+ }
1048
+ if (logalot) { console.log(`${lc} ackData: ${pretty(ackData)} (I: 7f8e9d0a1b2c3d4e5f6g7h8i9j0k)`); }
917
1049
 
918
- const deltaReqAddrs = ackData.deltaReqAddrs || [];
919
- const pushOfferAddrs = ackData.pushOfferAddrs || [];
1050
+ // 1. Check for Conflicts
1051
+ const conflicts = ackData.conflicts || [];
1052
+ console.log(`${lc} [CONFLICT DEBUG] Received conflicts from Ack: ${conflicts.length}`);
1053
+ if (conflicts.length > 0) {
1054
+ console.log(`${lc} [CONFLICT DEBUG] Conflicts detail: ${JSON.stringify(conflicts, null, 2)}`);
1055
+ }
920
1056
 
921
- // 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
922
- const pullReqAddrs: string[] = [];
923
- for (const addr of pushOfferAddrs) {
924
- const existing = srcGraph[addr] || (await getFromSpace({ addr, space })).ibGibs?.[0];
925
- if (!existing) {
926
- pullReqAddrs.push(addr);
1057
+ const terminalConflicts = conflicts.filter(c => c.terminal);
1058
+ if (terminalConflicts.length > 0) {
1059
+ console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`);
1060
+ // Terminal failure. Sender should probably Commit(Fail) or just Abort.
1061
+ // For now, throw to trigger abort.
1062
+ throw new Error(`${lc} Peer reported terminal conflicts. (E: a1b2c3d4e5f6g7h8i9j0k)`);
927
1063
  }
928
- }
929
1064
 
930
- // 2. Process Delta Requests (Push Payload)
931
- // [NEW] Smart Diff: Use knowledgeVector to skip dependencies
932
- const skipAddrs = new Set<string>();
933
- if (ackData.knowledgeVector) {
934
- Object.values(ackData.knowledgeVector).forEach(addrs => {
935
- addrs.forEach(a => skipAddrs.add(a));
936
- });
937
- }
1065
+ const optimisticConflicts = conflicts.filter(c => !c.terminal);
1066
+ const mergeDeltaReqs: string[] = []; // Additional requests for merging
1067
+
1068
+ if (optimisticConflicts.length > 0) {
1069
+ console.log(`${lc} [CONFLICT DEBUG] Processing ${optimisticConflicts.length} optimistic conflicts`);
1070
+ // We need to resolve these.
1071
+ // Strategy:
1072
+ // 1. Analyze Divergence (Sender vs Receiver)
1073
+ // 2. Identify missing data needed for merge (Receiver's unique frames)
1074
+ // 3. Request that data (as Delta Reqs)
1075
+ // 4. (Later in Delta Phase) Perform Merge.
1076
+
1077
+ // BUT: The Delta Phase is usually generic "Send me these Addrs".
1078
+ // If we just add to `deltaReqAddrs` (which are requests for Sender to send to Receiver?),
1079
+ // wait. `ackData.deltaReqAddrs` are what RECEIVER wants from SENDER.
1080
+
1081
+ // We (Sender) are processing the Ack.
1082
+ // We need to request data FROM Receiver.
1083
+ // But the protocol 'Ack' step typically leads to 'Delta' (Sender sending data).
1084
+
1085
+ // If Sender needs data from Receiver, it usually happens in 'Pull' mode or a separate request?
1086
+ // Or can we send a 'Delta Request' frame?
1087
+ // Standard Saga: Init(Push) -> Ack(Pull Reqs) -> Delta(Push Data).
1088
+
1089
+ // If Sender needs data, we might need a "Reverse Delta" or "Pull" phase?
1090
+ // Or we just proceed to Delta (sending what Receiver wants),
1091
+ // AND we piggyback our own requests?
1092
+ // OR: We treat the Conflict Resolution as a sub-saga or side-effect?
1093
+
1094
+ // SIMPLIFICATION for V1:
1095
+ // If we need data to merge, we must get it.
1096
+ // We are the Coordinator (Active). We can fetch from Peer immediately?
1097
+ // `peer.pull(addr)`?
1098
+ // Yes! The Coordinator has the `peer`.
1099
+
1100
+ // Let's analyze and pull immediately.
1101
+
1102
+ for (const conflict of optimisticConflicts) {
1103
+ const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
1104
+
1105
+ // Sender History
1106
+ // We need our own history for this timeline.
1107
+ // We know the 'senderTip' (remoteAddr in Ack).
1108
+ // Sender should verify it has this tip.
1109
+
1110
+ // Compute Diffs
1111
+ // We need to find `receiverOnly` addrs.
1112
+ // Receiver sent us `timelineAddrs` (Full History).
1113
+ const receiverHistorySet = new Set(timelineAddrs);
1114
+
1115
+ // We need our execution context's history for this senderTip.
1116
+ // We can fetch valid 'past' from space.
1117
+ const resSenderTip = await getFromSpace({ space: destSpace, addr: senderTip });
1118
+ const senderTipIbGib = resSenderTip.ibGibs?.[0];
1119
+ if (!senderTipIbGib) { throw new Error(`${lc} Sender missing its own tip? ${senderTip} (E: 9c8d7e6f5g4h3i2j1k0l)`); }
1120
+
1121
+ // Basic Diff: Find what Receiver has that we don't.
1122
+ // Actually, we need to traverse OUR past to find commonality.
1123
+ const senderHistory = [senderTip, ...(senderTipIbGib.rel8ns?.past || [])];
1124
+
1125
+ const receiverOnlyAddrs = timelineAddrs.filter(addr => !senderHistory.includes(addr));
1126
+
1127
+ if (receiverOnlyAddrs.length > 0) {
1128
+ console.log(`${lc} [CONFLICT DEBUG] Found ${receiverOnlyAddrs.length} receiver-only frames - need to pull for merge`);
1129
+ console.log(`${lc} [CONFLICT DEBUG] Receiver-only addrs:`, receiverOnlyAddrs);
1130
+ // PULL these frames from Peer into Local Space
1131
+ // (Validation: We trust peer for now / verification happens on put)
1132
+ for (const addr of receiverOnlyAddrs) {
1133
+ // This 'pull' is a sync-peer method?
1134
+ // The Coordinator 'peer' passed in 'sync()' might be needed here?
1135
+ // Wait, `handleAckFrame` doesn't have reference to `peer`?
1136
+ // It only has `space`, `metaspace`.
1137
+ // The `peer` is held by the `executeSagaLoop`.
1138
+
1139
+ // PROBLEM: `handleAckFrame` is pure logic on the Space/Data?
1140
+ // No, it's a method on Coordinator.
1141
+ // But `executeSagaLoop` calls it.
1142
+ // We might need to return "Requirements" to the loop?
1143
+
1144
+ // Checking return type: `{ frame: SyncIbGib_V1, payloadIbGibs?: ... }`
1145
+ // It returns the NEXT frame (Delta).
1146
+
1147
+ // If we need to fetch data, we are blocked.
1148
+ // We can't easily "Pull" here without the Peer reference.
1149
+
1150
+ // OPTION A: Pass `peer` to `handleAckFrame`.
1151
+ // OPTION B: Return a strict list of "MissingDeps" and let Loop handle it.
1152
+
1153
+ // Let's assume we can resolve this by adding `peer` to signature or using `metaspace` if it's a peer-witness?
1154
+ // No, Peer is ephemeral connection.
1155
+
1156
+ // Let's add `peer` to `handleAckFrame` signature?
1157
+ // It breaks the pattern of just handling frame + space.
1158
+
1159
+ // ALTERNATIVE: Use the `Delta` frame to request data?
1160
+ // `SyncSagaMessageDeltaData` has `requests?: string[]`.
1161
+ // Sender sends Delta Frame.
1162
+ // Does Receiver handle Delta Requests?
1163
+ // `handleDeltaFrame` (Receiver) -> checks `requests`.
1164
+ // YES.
1165
+
1166
+ // So Sender puts `receiverOnlyAddrs` into `deltaFrame.requests`.
1167
+ // Receiver sees them, fetches them, and includes them in the Response (Commit?).
1168
+ // Wait, Init->Ack->Delta->Commit.
1169
+ // If Receiver sends data in Commit, that's "too late" for Sender to Merge in THIS saga round?
1170
+ // Unless Commit is not the end?
1171
+
1172
+ // Or we do a "Delta 2" loop?
1173
+
1174
+ // "Iterative Resolution Loop" from plan.
1175
+ // If we request data in Delta, Receiver sends it in Commit (or Delta-Response).
1176
+ // Sender gets Commit. Sees data. Merges.
1177
+ // Then Sender needs to Send the MERGE result.
1178
+ // Needs another Push/Delta.
1179
+
1180
+ // REFINED FLOW:
1181
+ // 1. Sender sends Delta Frame with `requests: [receiverOnlyAddrs]`.
1182
+ // 2. Receiver responds (Commit? or Ack 2?) with Payload (Divergent Frames).
1183
+ // 3. Sender handles response -> Merges.
1184
+ // 4. Sender sends Commit (containing Merge Frame).
1185
+
1186
+ // Issue: Current state machine is Init->Ack->Delta->Commit.
1187
+ // We need to keep Saga open.
1188
+ // If Sender sends Delta with requests, does it transition to Commit?
1189
+ }
938
1190
 
939
- const payloadIbGibs: IbGib_V1[] = [];
940
- // Gather all tips to sync first
941
- const tipsToSync: IbGib_V1[] = [];
942
- for (const addr of deltaReqAddrs) {
943
- let ibGib = srcGraph[addr];
944
- if (!ibGib) {
945
- const res = await getFromSpace({ addr, space });
946
- if (res.ibGibs && res.ibGibs.length > 0) {
947
- ibGib = res.ibGibs[0];
1191
+ // Compute DELTA dependencies for each receiver-only frame
1192
+ // Find LCA to determine what dependencies we already have
1193
+ const lcaAddr = timelineAddrs.find(addr => senderHistory.includes(addr));
1194
+ console.log(`${lc} [CONFLICT DEBUG] LCA: ${lcaAddr || 'NONE'}`);
1195
+
1196
+ const skipAddrsSet = new Set<string>();
1197
+ if (lcaAddr) {
1198
+ try {
1199
+ const lcaRes = await getFromSpace({ addr: lcaAddr, space: destSpace });
1200
+ const lcaIbGib = lcaRes.ibGibs?.[0];
1201
+ if (lcaIbGib) {
1202
+ const lcaDeps = await getDependencyGraph({ ibGib: lcaIbGib, space: destSpace });
1203
+ if (lcaDeps) Object.keys(lcaDeps).forEach(a => skipAddrsSet.add(a));
1204
+ console.log(`${lc} [CONFLICT DEBUG] LCA deps to skip: ${skipAddrsSet.size}`);
1205
+ }
1206
+ } catch (e) {
1207
+ console.warn(`${lc} Error getting LCA deps: ${extractErrorMsg(e)}`);
1208
+ }
1209
+ }
1210
+
1211
+
1212
+ // For each receiver-only frame, get its DELTA dependency graph (minus LCA deps)
1213
+ for (const addr of receiverOnlyAddrs) {
1214
+ // Add the frame itself first
1215
+ if (!mergeDeltaReqs.includes(addr)) {
1216
+ mergeDeltaReqs.push(addr);
1217
+ }
1218
+
1219
+ // Get the frame's delta dependencies (skip LCA's deps)
1220
+ try {
1221
+ const frameRes = await getFromSpace({ addr, space: destSpace });
1222
+ const frameIbGib = frameRes.ibGibs?.[0];
1223
+
1224
+ if (frameIbGib) {
1225
+ // Get dependency graph, skipping all LCA dependencies
1226
+ const frameDeltaDeps = await getDependencyGraph({
1227
+ ibGib: frameIbGib,
1228
+ space: destSpace,
1229
+ skipAddrs: Array.from(skipAddrsSet), // Skip entire LCA dep graph
1230
+ });
1231
+
1232
+ if (frameDeltaDeps) {
1233
+ // Add all delta dependencies (Object.keys gives us the addresses)
1234
+ Object.keys(frameDeltaDeps).forEach(depAddr => {
1235
+ if (!mergeDeltaReqs.includes(depAddr) && !skipAddrsSet.has(depAddr)) {
1236
+ mergeDeltaReqs.push(depAddr);
1237
+ }
1238
+ });
1239
+ }
1240
+ }
1241
+ } catch (depError) {
1242
+ console.warn(`${lc} [CONFLICT DEBUG] Error getting delta deps for ${addr}: ${extractErrorMsg(depError)}`);
1243
+ }
1244
+ }
1245
+
1246
+ console.log(`${lc} [CONFLICT DEBUG] Total merge requests (frames + delta deps): ${mergeDeltaReqs.length}`);
1247
+ } else {
1248
+ console.log(`${lc} [CONFLICT DEBUG] No receiver-only frames found for this conflict`);
1249
+ }
948
1250
  }
949
- }
950
- if (ibGib) {
951
- tipsToSync.push(ibGib);
1251
+
1252
+ console.log(`${lc} [CONFLICT DEBUG] Finished processing ${optimisticConflicts.length} conflicts. mergeDeltaReqs: ${mergeDeltaReqs.length}`);
952
1253
  } else {
953
- throw new Error(`${lc} Requested addr not found: ${addr} (E: d41d59cff4a887f6414c3e92eabd8e26)`);
1254
+ console.log(`${lc} [CONFLICT DEBUG] No optimistic conflicts to process`);
954
1255
  }
955
- }
956
1256
 
957
- // Calculate Dependency Graph for ALL tips, effectively utilizing common history
958
- // Pass skipAddrs to `getDependencyGraph` or gather manually.
959
- // `getDependencyGraph` takes a single ibGib.
960
- // We can optimize by doing it for each tip and unioning the result?
961
- // Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
962
- // We will loop.
963
-
964
- const allDepsSet = new Set<string>();
965
-
966
- for (const tip of tipsToSync) {
967
- // Always include the tip itself
968
- const tipAddr = getIbGibAddr({ ibGib: tip });
969
- // Only process if not skipped (though deltaReq implies they barely just asked for it)
970
- // But detailed deps might be skipped.
971
-
972
- // Get Graph with Skips
973
- // Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
974
- const deps = await getDependencyGraph({
975
- ibGib: tip,
976
- space,
977
- skipAddrs: Array.from(skipAddrs)
978
- });
1257
+ // 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
979
1258
 
980
- // [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
981
- let tipIncluded = false;
1259
+ const deltaReqAddrs = ackData.deltaReqAddrs || [];
1260
+ const pushOfferAddrs = ackData.pushOfferAddrs || [];
982
1261
 
983
- if (deps) {
984
- Object.values(deps).forEach(d => {
985
- const dAddr = getIbGibAddr({ ibGib: d });
986
- if (!allDepsSet.has(dAddr)) {
987
- allDepsSet.add(dAddr);
988
- payloadIbGibs.push(d);
989
- }
990
- if (dAddr === tipAddr) { tipIncluded = true; }
1262
+ // 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
1263
+ const pullReqAddrs: string[] = [];
1264
+ for (const addr of pushOfferAddrs) {
1265
+ const existing = srcGraph[addr] || (await getFromSpace({ addr, space: destSpace })).ibGibs?.[0];
1266
+ if (!existing) {
1267
+ pullReqAddrs.push(addr);
1268
+ }
1269
+ }
1270
+
1271
+ // 2. Process Delta Requests (Push Payload)
1272
+ // [NEW] Smart Diff: Use knowledgeVector to skip dependencies
1273
+ const skipAddrs = new Set<string>();
1274
+ if (ackData.knowledgeVector) {
1275
+ Object.values(ackData.knowledgeVector).forEach(addrs => {
1276
+ addrs.forEach(a => skipAddrs.add(a));
991
1277
  });
992
1278
  }
993
1279
 
994
- if (!tipIncluded && !skipAddrs.has(tipAddr)) {
995
- if (logalot) { console.log(`${lc} Tip not in deps, adding explicitly: ${tipAddr}`); }
996
- if (!allDepsSet.has(tipAddr)) {
997
- allDepsSet.add(tipAddr);
998
- payloadIbGibs.push(tip);
1280
+ const payloadIbGibs: IbGib_V1[] = [];
1281
+ // Gather all tips to sync first
1282
+ const tipsToSync: IbGib_V1[] = [];
1283
+ for (const addr of deltaReqAddrs) {
1284
+ let ibGib = srcGraph[addr];
1285
+ if (!ibGib) {
1286
+ const res = await getFromSpace({ addr, space: destSpace });
1287
+ if (res.ibGibs && res.ibGibs.length > 0) {
1288
+ ibGib = res.ibGibs[0];
1289
+ }
1290
+ }
1291
+ if (ibGib) {
1292
+ tipsToSync.push(ibGib);
1293
+ } else {
1294
+ throw new Error(`${lc} Requested addr not found: ${addr} (E: d41d59cff4a887f6414c3e92eabd8e26)`);
999
1295
  }
1000
1296
  }
1001
- }
1002
1297
 
1003
- // 3. Create Delta Frame
1004
- const sagaId = ackData.sagaId;
1005
- const deltaData: SyncSagaMessageDeltaData_V1 = {
1006
- sagaId,
1007
- stage: SyncStage.delta,
1008
- payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
1009
- requests: pullReqAddrs.length > 0 ? pullReqAddrs : undefined,
1010
- };
1298
+ // Calculate Dependency Graph for ALL tips, effectively utilizing common history
1299
+ // Pass skipAddrs to `getDependencyGraph` or gather manually.
1300
+ // `getDependencyGraph` takes a single ibGib.
1301
+ // We can optimize by doing it for each tip and unioning the result?
1302
+ // Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
1303
+ // We will loop.
1304
+
1305
+ const allDepsSet = new Set<string>();
1306
+
1307
+ for (const tip of tipsToSync) {
1308
+ // Always include the tip itself
1309
+ const tipAddr = getIbGibAddr({ ibGib: tip });
1310
+ // Only process if not skipped (though deltaReq implies they barely just asked for it)
1311
+ // But detailed deps might be skipped.
1312
+
1313
+ // Get Graph with Skips
1314
+ // Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
1315
+ const deps = await getDependencyGraph({
1316
+ ibGib: tip,
1317
+ space: destSpace,
1318
+ skipAddrs: Array.from(skipAddrs)
1319
+ });
1011
1320
 
1012
- const deltaStone = await this.createSyncMsgStone({
1013
- data: deltaData,
1014
- space,
1015
- metaspace,
1016
- });
1321
+ // [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
1322
+ let tipIncluded = false;
1017
1323
 
1018
- const deltaFrame = await this.evolveSyncSagaIbGib({
1019
- prevSagaIbGib: sagaIbGib,
1020
- msgStones: [deltaStone],
1021
- identity,
1022
- space,
1023
- metaspace,
1024
- });
1324
+ if (deps) {
1325
+ Object.values(deps).forEach(d => {
1326
+ const dAddr = getIbGibAddr({ ibGib: d });
1327
+ if (!allDepsSet.has(dAddr)) {
1328
+ allDepsSet.add(dAddr);
1329
+ payloadIbGibs.push(d);
1330
+ }
1331
+ if (dAddr === tipAddr) { tipIncluded = true; }
1332
+ });
1333
+ }
1025
1334
 
1026
- return { frame: deltaFrame, payloadIbGibs };
1335
+ if (!tipIncluded && !skipAddrs.has(tipAddr)) {
1336
+ if (logalot) { console.log(`${lc} Tip not in deps, adding explicitly: ${tipAddr}`); }
1337
+ if (!allDepsSet.has(tipAddr)) {
1338
+ allDepsSet.add(tipAddr);
1339
+ payloadIbGibs.push(tip);
1340
+ }
1341
+ }
1342
+ }
1343
+
1344
+ // 3. Create Delta Frame
1345
+ const sagaId = ackData.sagaId;
1346
+ const deltaData: SyncSagaMessageDeltaData_V1 = {
1347
+ sagaId: sagaIbGib.data!.uuid,
1348
+ stage: SyncStage.delta,
1349
+ payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
1350
+ requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
1351
+ };
1352
+
1353
+ if (logalot) { console.log(`${lc} Creating Delta Stone. Data stage: ${deltaData.stage}`); }
1354
+
1355
+ const deltaStone = await this.createSyncMsgStone({
1356
+ data: deltaData,
1357
+ space: tempSpace,
1358
+ metaspace,
1359
+ });
1360
+
1361
+ const deltaFrame = await this.evolveSyncSagaIbGib({
1362
+ prevSagaIbGib: sagaIbGib,
1363
+ msgStones: [deltaStone],
1364
+ identity,
1365
+ space: tempSpace,
1366
+ metaspace,
1367
+ });
1368
+
1369
+ // IMMEDIATELY persist to both spaces for audit trail
1370
+ await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
1371
+
1372
+ if (logalot) { console.log(`${lc} Delta Frame created. Rel8ns: ${JSON.stringify(deltaFrame.rel8ns)}`); }
1373
+
1374
+ return { frame: deltaFrame, payloadIbGibs };
1375
+ } catch (error) {
1376
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1377
+ throw error;
1378
+ } finally {
1379
+ if (logalot) { console.log(`${lc} complete.`); }
1380
+ }
1027
1381
  }
1028
1382
 
1029
1383
  /**
@@ -1039,20 +1393,22 @@ export class SyncSagaCoordinator {
1039
1393
  protected async handleDeltaFrame({
1040
1394
  sagaIbGib,
1041
1395
  srcGraph,
1042
- space,
1396
+ destSpace,
1397
+ tempSpace,
1043
1398
  metaspace,
1044
1399
  identity,
1045
1400
  }: {
1046
1401
  sagaIbGib: SyncIbGib_V1,
1047
1402
  srcGraph: { [addr: string]: IbGib_V1 },
1048
- space: IbGibSpaceAny,
1403
+ destSpace: IbGibSpaceAny,
1404
+ tempSpace: IbGibSpaceAny,
1049
1405
  metaspace: MetaspaceService,
1050
1406
  identity?: KeystoneIbGib_V1,
1051
1407
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[], receivedPayloadIbGibs?: IbGib_V1[] } | null> {
1052
1408
  const lc = `${this.lc}[${this.handleDeltaFrame.name}]`;
1053
1409
  if (logalot) { console.log(`${lc} starting...`); }
1054
1410
 
1055
- const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
1411
+ const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
1056
1412
  const deltaData = messageData as SyncSagaMessageDeltaData_V1;
1057
1413
 
1058
1414
  if (!deltaData) {
@@ -1061,50 +1417,204 @@ export class SyncSagaCoordinator {
1061
1417
  if (deltaData.stage !== SyncStage.delta) {
1062
1418
  throw new Error(`${lc} Invalid delta frame: deltaData.stage !== SyncStage.delta (E: 0c28c8d8f08a4421b8344e6727271421)`);
1063
1419
  }
1420
+ if (logalot) { console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`); }
1421
+
1422
+ console.log(`${lc} [CONFLICT DEBUG] deltaData.payloadAddrs count: ${deltaData.payloadAddrs?.length || 0}`);
1064
1423
 
1065
1424
  const payloadAddrs = deltaData.payloadAddrs || [];
1066
- const requests = deltaData.requests || [];
1425
+ const peerRequests = deltaData.requests || [];
1426
+ const peerProposesCommit = deltaData.proposeCommit || false;
1067
1427
 
1068
- // 1. Process Received Payload
1428
+ // 1. Process Received Payload (Ingest)
1069
1429
  const receivedPayloadIbGibs: IbGib_V1[] = [];
1070
1430
  if (payloadAddrs.length > 0) {
1431
+ // We use `payloadAddrs` as the manifest.
1432
+ // The ACTUAL collection of ibGibs should be available via `getFromSpace`
1433
+ // assuming the "Transport" layer put them there implicitly?
1434
+ // OR, if we are local-only, we just get them.
1435
+ // The `handleDeltaFrame` contract assumes data is reachable in `space`.
1436
+
1071
1437
  const res = await getFromSpace({
1072
1438
  addrs: payloadAddrs,
1073
- space,
1439
+ space: tempSpace, // Incoming data is in tempSpace
1074
1440
  });
1075
1441
  if (res.ibGibs) {
1076
1442
  receivedPayloadIbGibs.push(...res.ibGibs);
1443
+ // Also put them? `getFromSpace` retrieves. If they are in space, they are persisted.
1444
+ // If this is a Temp Space, they are safe.
1445
+ } else {
1446
+ console.warn(`${lc} Failed to retrieve payloads listed in delta: ${payloadAddrs.join(', ')}`);
1077
1447
  }
1078
1448
  }
1079
1449
 
1080
- // 2. Fulfill Requests (Outgoing Payload)
1450
+ // 2. Fulfill Peer Requests (Outgoing Payload with Delta Dependencies)
1081
1451
  const outgoingPayload: IbGib_V1[] = [];
1082
- for (const addr of requests) {
1452
+ const outgoingAddrsSet = new Set<string>(); // Track what we've added
1453
+
1454
+ console.log(`${lc} [CONFLICT DEBUG] Fulfilling ${peerRequests.length} peer requests`);
1455
+
1456
+ for (const addr of peerRequests) {
1457
+ // Get the requested ibGib
1083
1458
  let ibGib = srcGraph[addr];
1084
1459
  if (!ibGib) {
1085
- const res = await getFromSpace({ addr, space });
1460
+ const res = await getFromSpace({ addr, space: destSpace }); // Query from destSpace
1086
1461
  if (res.ibGibs && res.ibGibs.length > 0) {
1087
1462
  ibGib = res.ibGibs[0];
1088
1463
  }
1089
1464
  }
1465
+
1090
1466
  if (ibGib) {
1091
- outgoingPayload.push(ibGib);
1467
+ // Add the requested ibGib itself
1468
+ const ibGibAddr = getIbGibAddr({ ibGib });
1469
+ if (!outgoingAddrsSet.has(ibGibAddr)) {
1470
+ outgoingPayload.push(ibGib);
1471
+ outgoingAddrsSet.add(ibGibAddr);
1472
+ }
1473
+
1474
+ // Expand to include full dependency graph for this ibGib
1475
+ // (Receiver needs all deps to properly process/merge)
1476
+ try {
1477
+ const deps = await getDependencyGraph({
1478
+ ibGib,
1479
+ space: destSpace,
1480
+ });
1481
+
1482
+ if (deps) {
1483
+ Object.values(deps).forEach(depIbGib => {
1484
+ const depAddr = getIbGibAddr({ ibGib: depIbGib });
1485
+ if (!outgoingAddrsSet.has(depAddr)) {
1486
+ outgoingPayload.push(depIbGib);
1487
+ outgoingAddrsSet.add(depAddr);
1488
+ }
1489
+ });
1490
+ }
1491
+ } catch (depError) {
1492
+ console.warn(`${lc} [CONFLICT DEBUG] Error expanding deps for ${addr}: ${extractErrorMsg(depError)}`);
1493
+ }
1494
+ } else {
1495
+ console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
1092
1496
  }
1093
1497
  }
1094
1498
 
1095
- // 3. Determine Next Stage
1096
- if (requests.length > 0) {
1097
- // They requested more data -> Send Delta
1098
- const sagaId = deltaData.sagaId;
1499
+ console.log(`${lc} [CONFLICT DEBUG] Outgoing payload size (with deps): ${outgoingPayload.length}`);
1500
+
1501
+
1502
+ // 3. Execute Merges (If applicable)
1503
+ // Check if we have pending conflicts that we CAN resolve now that we have data.
1504
+ // We look at the Saga History (Ack Frame) to find conflicts.
1505
+ // Optimization: Do this only if we received payloads.
1506
+ const mergeResultIbGibs: IbGib_V1[] = [];
1507
+
1508
+ console.log(`${lc} [CONFLICT DEBUG] Checking for merge. receivedPayloadIbGibs.length: ${receivedPayloadIbGibs.length}`);
1509
+
1510
+ if (receivedPayloadIbGibs.length > 0) {
1511
+ console.log(`${lc} [TEST DEBUG] Received Payloads (${receivedPayloadIbGibs.length}). Checking for conflicts/merges...`);
1512
+ // Find the Ack frame in history to get conflicts
1513
+ // Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
1514
+ // V1 timelines carry full history in `past`.
1515
+ const pastAddrs = sagaIbGib.rel8ns?.past || [];
1516
+ console.log(`${lc} [TEST DEBUG] pastAddrs count: ${pastAddrs.length}`);
1517
+ let ackData: SyncSagaMessageAckData_V1 | undefined;
1518
+
1519
+ if (pastAddrs.length > 0) {
1520
+ // Batch fetch all past frames
1521
+ const resPast = await getFromSpace({ addrs: pastAddrs, space: tempSpace });
1522
+ if (resPast.success && resPast.ibGibs) {
1523
+ // Iterate backwards (most recent first) to find the latest Ack
1524
+ for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
1525
+ const pastFrame = resPast.ibGibs[i];
1526
+ const messageStone = await getSyncSagaMessageFromFrame({
1527
+ frameIbGib: pastFrame,
1528
+ space: tempSpace
1529
+ });
1530
+ if (messageStone?.data?.stage === SyncStage.ack) {
1531
+ ackData = messageStone.data as SyncSagaMessageAckData_V1;
1532
+ console.log(`${lc} [TEST DEBUG] Found Ack Frame. Conflicts: ${ackData.conflicts?.length || 0}`);
1533
+ break;
1534
+ }
1535
+ }
1536
+ }
1537
+ }
1538
+
1539
+ if (ackData && ackData.conflicts) {
1540
+ const optimisticConflicts = ackData.conflicts.filter(c => !c.terminal);
1541
+ for (const conflict of optimisticConflicts) {
1542
+ const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
1543
+ // We are Sender (usually) here if we are merging.
1544
+ // Check if we have the history needed (timelineAddrs).
1545
+ // Specifically, we needed the `receiverOnly` parts.
1546
+
1547
+ // We blindly attempt merge if we have both tips accessible?
1548
+ // We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
1549
+
1550
+ // Check if we have receiverTip in space
1551
+ console.log(`${lc} [CONFLICT DEBUG] Attempting merge for conflict. ReceiverTip: ${receiverTip}, SenderTip: ${senderTip}`);
1552
+ const resRecTip = await getFromSpace({ addr: receiverTip, space: tempSpace }); // Check tempSpace for incoming data
1553
+ console.log(`${lc} [CONFLICT DEBUG] ReceiverTip found in tempSpace: ${!!resRecTip.ibGibs?.[0]}`);
1554
+ if (resRecTip.success && resRecTip.ibGibs?.[0]) {
1555
+ // We have the tip!
1556
+ // Do we have the full history?
1557
+ // `mergeDivergentTimelines` in `conflict-optimistic` will attempt to fetch history.
1558
+ // If we just ingested the missing pieces, `getFromSpace` inside `merge` should succeed.
1559
+
1560
+ // Perform Merge!
1561
+ try {
1562
+ const mergeResult = await mergeDivergentTimelines({
1563
+ tipA: (await getFromSpace({ addr: senderTip, space: destSpace })).ibGibs![0], // Our tip from destSpace
1564
+ tipB: resRecTip.ibGibs[0], // Their tip (from tempSpace)
1565
+ space: tempSpace, // Merge uses tempSpace
1566
+ metaspace,
1567
+ });
1568
+ if (mergeResult) {
1569
+ console.log(`${lc} [TEST DEBUG] Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
1570
+ if (logalot) { console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`); }
1571
+ mergeResultIbGibs.push(mergeResult);
1572
+ outgoingPayload.push(mergeResult); // Send result to peer
1573
+ }
1574
+ } catch (e) {
1575
+ console.error(`${lc} Merge failed: ${e}`);
1576
+ // If merge fails, we might Abort or just continue?
1577
+ }
1578
+ }
1579
+ }
1580
+ }
1581
+ }
1582
+
1583
+ // 4. Determine Next Action
1584
+ // We have `outgoingPayload` (Requests + Merge Results).
1585
+ // Does Peer have outstanding requests? No, we fulfilled `peerRequests`.
1586
+ // Do WE have outstanding requests?
1587
+ // We might if `mergeResult` requires further sync? Usually no, result is complete.
1588
+
1589
+ const myRequests: string[] = []; // If we had more needs (e.g. partial payload), we'd add here.
1590
+
1591
+ const hasOutgoing = outgoingPayload.length > 0;
1592
+ const hasMyRequests = myRequests.length > 0;
1593
+
1594
+ if (hasOutgoing || hasMyRequests) {
1595
+ // We have business to attend to -> Send Delta
1099
1596
  const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
1100
- sagaId,
1597
+ sagaId: deltaData.sagaId,
1101
1598
  stage: SyncStage.delta,
1102
1599
  payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
1600
+ requests: hasMyRequests ? myRequests : undefined,
1601
+ proposeCommit: !hasMyRequests // If we are sending data but have no requests, we VALIDATE PROPOSAL?
1602
+ // Wait. If we send data, we are NOT committing yet.
1603
+ // We are sending data. The OTHER side must ingest it.
1604
+ // So proposeCommit = true?
1605
+ // "Here is the data. I'm done. If you are good, let's commit."
1606
+ // Yes.
1103
1607
  };
1104
1608
 
1609
+ // BUT if `peerProposesCommit` was true, and we are sending data, we are effectively rejecting/delaying it.
1610
+ // We just send the Delta. Peer receives it, ingests, sees ProposeCommit=True (from us), and then Commits.
1611
+
1612
+ // So yes, proposeCommit = true.
1613
+ responseDeltaData.proposeCommit = true;
1614
+
1105
1615
  const deltaStone = await this.createSyncMsgStone({
1106
1616
  data: responseDeltaData,
1107
- space,
1617
+ space: tempSpace,
1108
1618
  metaspace
1109
1619
  });
1110
1620
 
@@ -1112,69 +1622,138 @@ export class SyncSagaCoordinator {
1112
1622
  prevSagaIbGib: sagaIbGib,
1113
1623
  msgStones: [deltaStone],
1114
1624
  identity,
1115
- space,
1625
+ space: tempSpace,
1116
1626
  metaspace
1117
1627
  });
1118
1628
 
1629
+ // IMMEDIATELY persist to both spaces for audit trail
1630
+ await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
1631
+
1119
1632
  return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
1120
1633
 
1121
1634
  } 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
- };
1635
+ // We have nothing to send.
1636
+
1637
+ if (peerProposesCommit) {
1638
+ // Peer is done. We are done. -> Commit.
1639
+ const commitData: SyncSagaMessageCommitData_V1 = {
1640
+ sagaId: deltaData.sagaId,
1641
+ stage: SyncStage.commit,
1642
+ success: true,
1643
+ };
1129
1644
 
1130
- const commitStone = await this.createSyncMsgStone({
1131
- data: commitData,
1132
- space,
1133
- metaspace
1134
- });
1645
+ const commitStone = await this.createSyncMsgStone({
1646
+ data: commitData,
1647
+ space: tempSpace,
1648
+ metaspace
1649
+ });
1135
1650
 
1136
- const commitFrame = await this.evolveSyncSagaIbGib({
1137
- prevSagaIbGib: sagaIbGib,
1138
- msgStones: [commitStone],
1139
- identity,
1140
- space,
1141
- metaspace
1142
- });
1651
+ const commitFrame = await this.evolveSyncSagaIbGib({
1652
+ prevSagaIbGib: sagaIbGib,
1653
+ msgStones: [commitStone],
1654
+ identity,
1655
+ space: tempSpace,
1656
+ metaspace
1657
+ });
1658
+
1659
+ // IMMEDIATELY persist to both spaces for audit trail
1660
+ await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
1661
+
1662
+ return { frame: commitFrame, receivedPayloadIbGibs };
1663
+
1664
+ } else {
1665
+ // peer did NOT propose commit (maybe they just sent data/requests and didn't ready flag).
1666
+ // But we are empty.
1667
+ // So WE propose commit.
1668
+ const responseDeltaData: SyncSagaMessageDeltaData_V1 = {
1669
+ sagaId: deltaData.sagaId,
1670
+ stage: SyncStage.delta,
1671
+ proposeCommit: true,
1672
+ payloadAddrs: [], // Always include empty array if sending delta
1673
+ };
1674
+
1675
+ const deltaStone = await this.createSyncMsgStone({
1676
+ data: responseDeltaData,
1677
+ space: tempSpace,
1678
+ metaspace
1679
+ });
1680
+
1681
+ const deltaFrame = await this.evolveSyncSagaIbGib({
1682
+ prevSagaIbGib: sagaIbGib,
1683
+ msgStones: [deltaStone],
1684
+ identity,
1685
+ space: tempSpace,
1686
+ metaspace
1687
+ });
1143
1688
 
1144
- return { frame: commitFrame, receivedPayloadIbGibs };
1689
+ // IMMEDIATELY persist to both spaces for audit trail
1690
+ await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
1691
+
1692
+ // Check if PEER proposed commit
1693
+ if (deltaData.proposeCommit) {
1694
+ if (logalot) { console.log(`${lc} Peer proposed commit. Accepting & Committing.`); }
1695
+ // Peer wants to commit and has no more requests.
1696
+ // We should Commit.
1697
+
1698
+ const commitData: SyncSagaMessageCommitData_V1 = {
1699
+ sagaId: deltaData.sagaId,
1700
+ stage: SyncStage.commit,
1701
+ success: true,
1702
+ };
1703
+
1704
+ const commitStone = await this.createSyncMsgStone({
1705
+ data: commitData,
1706
+ space: tempSpace,
1707
+ metaspace
1708
+ });
1709
+
1710
+ const commitFrame = await this.evolveSyncSagaIbGib({
1711
+ prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
1712
+ msgStones: [commitStone],
1713
+ identity,
1714
+ space: tempSpace,
1715
+ metaspace
1716
+ });
1717
+
1718
+ // IMMEDIATELY persist to both spaces for audit trail
1719
+ await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
1720
+
1721
+ return { frame: commitFrame, receivedPayloadIbGibs };
1722
+ }
1723
+
1724
+ return { frame: deltaFrame, receivedPayloadIbGibs };
1725
+ }
1145
1726
  }
1146
1727
  }
1147
1728
 
1729
+
1148
1730
  protected async handleCommitFrame({
1149
1731
  sagaIbGib,
1150
- space,
1732
+ destSpace,
1733
+ tempSpace,
1734
+ metaspace,
1735
+ identity,
1151
1736
  }: {
1152
1737
  sagaIbGib: SyncIbGib_V1,
1153
- space: IbGibSpaceAny,
1738
+ destSpace: IbGibSpaceAny,
1739
+ tempSpace: IbGibSpaceAny,
1740
+ metaspace: MetaspaceService,
1741
+ identity?: KeystoneIbGib_V1,
1154
1742
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
1155
1743
  const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
1156
- if (logalot) { console.log(`${lc} Commit received. Saga complete.`); }
1157
- return null;
1158
- }
1159
-
1160
- protected async handleConflictFrame({
1161
- sagaIbGib,
1162
- space,
1163
- }: {
1164
- sagaIbGib: SyncIbGib_V1,
1165
- space: IbGibSpaceAny,
1166
- }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
1167
- const lc = `${this.lc}[${this.handleConflictFrame.name}]`;
1168
- const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
1169
- const conflictData = messageData as SyncSagaMessageConflictData_V1;
1744
+ if (logalot) { console.log(`${lc} Commit received.`); }
1170
1745
 
1171
- if (logalot) { console.log(`${lc} Conflict received. Strategy: ${conflictData?.conflictStrategy}. Terminal: ${conflictData?.isTerminal}`); }
1746
+ // Sender Logic (Finalizing):
1747
+ // If we are here, we received a Commit frame from the Peer.
1748
+ // This implies the Peer has successfully committed.
1749
+ // We should now:
1750
+ // 1. Validate (implicitly done by receiving valid frame)
1751
+ // 2. Perform our own cleanup (Temp -> Dest, if applicable)
1752
+ // 3. Return null to signal saga completion.
1172
1753
 
1173
- if (conflictData?.isTerminal) {
1174
- throw new Error(`${lc} Saga aborted due to conflicts: ${JSON.stringify(conflictData.conflicts)} (E: b08d1f2a3c4e5d6f7a8b9c0d1e2f3a4b)`);
1175
- }
1754
+ // Note: Currently we don't have explicit cleanup logic implemented here yet (TODO).
1176
1755
 
1177
- // Non-terminal logic (stub for future)
1756
+ if (logalot) { console.log(`${lc} Peer committed. Finalizing saga locally. Saga Complete.`); }
1178
1757
  return null;
1179
1758
  }
1180
1759
 
@@ -1189,19 +1768,74 @@ export class SyncSagaCoordinator {
1189
1768
  space: IbGibSpaceAny,
1190
1769
  metaspace: MetaspaceService,
1191
1770
  }): 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>;
1771
+ const lc = `${this.lc}[${this.createSyncMsgStone.name}]`;
1772
+ try {
1773
+ if (logalot) { console.log(`${lc} starting... (I: 5f7f98e8ff980364f7191fcee4531e26)`); }
1774
+
1775
+ const ib = await getSyncSagaMessageIb({ data });
1776
+ const stone = await Factory_V1.stone({
1777
+ ib,
1778
+ parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
1779
+ data,
1780
+ uuid: true, // we want the stone to have its own uniqueness
1781
+ });
1782
+ if (logalot) { console.log(`${lc} Created stone: ${getIbGibAddr({ ibGib: stone })}`); }
1783
+ await putInSpace({ space, ibGib: stone });
1784
+ await metaspace.registerNewIbGib({ ibGib: stone });
1785
+ return stone as IbGib_V1<TStoneData>;
1786
+ } catch (error) {
1787
+ console.error(`${lc} ${extractErrorMsg(error)}`);
1788
+ throw error;
1789
+ } finally {
1790
+ if (logalot) { console.log(`${lc} complete.`); }
1791
+ }
1202
1792
  }
1203
1793
 
1204
1794
 
1795
+ /**
1796
+ * Ensures saga frame and its msg stone(s) are in BOTH spaces for audit trail.
1797
+ * Control ibgibs (saga frames, msg stones, identity) must be in both destSpace and tempSpace.
1798
+ */
1799
+ protected async ensureSagaFrameInBothSpaces({
1800
+ frame,
1801
+ destSpace,
1802
+ tempSpace,
1803
+ metaspace,
1804
+ }: {
1805
+ frame: SyncIbGib_V1,
1806
+ destSpace: IbGibSpaceAny,
1807
+ tempSpace: IbGibSpaceAny,
1808
+ metaspace: MetaspaceService,
1809
+ }): Promise<void> {
1810
+ // Frame itself (already in tempSpace from creation, need in destSpace for audit)
1811
+ await putInSpace({ space: destSpace, ibGib: frame });
1812
+ await metaspace.registerNewIbGib({ ibGib: frame });
1813
+
1814
+ // Msg stone(s) (already in tempSpace, need in destSpace for audit)
1815
+ const msgStoneAddrs = frame.rel8ns?.[SYNC_MSG_REL8N_NAME];
1816
+ if (msgStoneAddrs && msgStoneAddrs.length > 0) {
1817
+ const resMsgStones = await getFromSpace({ space: tempSpace, addrs: msgStoneAddrs });
1818
+ if (resMsgStones.ibGibs) {
1819
+ for (const msgStone of resMsgStones.ibGibs) {
1820
+ await putInSpace({ space: destSpace, ibGib: msgStone });
1821
+ await metaspace.registerNewIbGib({ ibGib: msgStone });
1822
+ }
1823
+ }
1824
+ }
1825
+
1826
+ // Identity (if present, already in tempSpace, need in destSpace)
1827
+ const identityAddrs = frame.rel8ns?.identity;
1828
+ if (identityAddrs && identityAddrs.length > 0) {
1829
+ const resIdentity = await getFromSpace({ space: tempSpace, addrs: identityAddrs });
1830
+ if (resIdentity.ibGibs) {
1831
+ for (const identity of resIdentity.ibGibs) {
1832
+ await putInSpace({ space: destSpace, ibGib: identity });
1833
+ await metaspace.registerNewIbGib({ ibGib: identity });
1834
+ }
1835
+ }
1836
+ }
1837
+ }
1838
+
1205
1839
  /**
1206
1840
  * Evolves the saga timeline with a new frame.
1207
1841
  */
@@ -1211,12 +1845,14 @@ export class SyncSagaCoordinator {
1211
1845
  identity,
1212
1846
  space,
1213
1847
  metaspace,
1848
+ conflictStrategy,
1214
1849
  }: {
1215
1850
  prevSagaIbGib?: SyncIbGib_V1,
1216
1851
  msgStones: IbGib_V1[],
1217
1852
  identity?: KeystoneIbGib_V1,
1218
1853
  space: IbGibSpaceAny,
1219
1854
  metaspace: MetaspaceService,
1855
+ conflictStrategy?: SyncConflictStrategy,
1220
1856
  }): Promise<SyncIbGib_V1> {
1221
1857
  const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
1222
1858
  try {
@@ -1293,6 +1929,7 @@ export class SyncSagaCoordinator {
1293
1929
  payload: undefined, // Data in stone
1294
1930
  n: 0,
1295
1931
  isTjp: true,
1932
+ conflictStrategy,
1296
1933
  };
1297
1934
  const ib = await getSyncIb({ data });
1298
1935