@ibgib/core-gib 0.1.20 → 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 (73) hide show
  1. package/dist/sync/graft-info/graft-info-constants.d.mts +5 -0
  2. package/dist/sync/graft-info/graft-info-constants.d.mts.map +1 -0
  3. package/dist/sync/graft-info/graft-info-constants.mjs +5 -0
  4. package/dist/sync/graft-info/graft-info-constants.mjs.map +1 -0
  5. package/dist/sync/graft-info/graft-info-helpers.d.mts +49 -0
  6. package/dist/sync/graft-info/graft-info-helpers.d.mts.map +1 -0
  7. package/dist/sync/graft-info/graft-info-helpers.mjs +236 -0
  8. package/dist/sync/graft-info/graft-info-helpers.mjs.map +1 -0
  9. package/dist/sync/graft-info/graft-info-helpers.respec.d.mts +2 -0
  10. package/dist/sync/graft-info/graft-info-helpers.respec.d.mts.map +1 -0
  11. package/dist/sync/graft-info/graft-info-helpers.respec.mjs +70 -0
  12. package/dist/sync/graft-info/graft-info-helpers.respec.mjs.map +1 -0
  13. package/dist/sync/graft-info/graft-info-types.d.mts +31 -0
  14. package/dist/sync/{merge-info/merge-info-types.d.mts.map → graft-info/graft-info-types.d.mts.map} +1 -1
  15. package/dist/sync/graft-info/graft-info-types.mjs +2 -0
  16. package/dist/sync/graft-info/graft-info-types.mjs.map +1 -0
  17. package/dist/sync/strategies/conflict-optimistic.d.mts +1 -1
  18. package/dist/sync/strategies/conflict-optimistic.d.mts.map +1 -1
  19. package/dist/sync/strategies/conflict-optimistic.mjs +10 -60
  20. package/dist/sync/strategies/conflict-optimistic.mjs.map +1 -1
  21. package/dist/sync/sync-conflict.respec.mjs +152 -33
  22. package/dist/sync/sync-conflict.respec.mjs.map +1 -1
  23. package/dist/sync/sync-constants.d.mts +1 -3
  24. package/dist/sync/sync-constants.d.mts.map +1 -1
  25. package/dist/sync/sync-constants.mjs +0 -2
  26. package/dist/sync/sync-constants.mjs.map +1 -1
  27. package/dist/sync/sync-peer/sync-peer-innerspace-v1.d.mts +5 -2
  28. package/dist/sync/sync-peer/sync-peer-innerspace-v1.d.mts.map +1 -1
  29. package/dist/sync/sync-peer/sync-peer-innerspace-v1.mjs +70 -7
  30. package/dist/sync/sync-peer/sync-peer-innerspace-v1.mjs.map +1 -1
  31. package/dist/sync/sync-saga-coordinator.d.mts +25 -18
  32. package/dist/sync/sync-saga-coordinator.d.mts.map +1 -1
  33. package/dist/sync/sync-saga-coordinator.mjs +508 -316
  34. package/dist/sync/sync-saga-coordinator.mjs.map +1 -1
  35. package/dist/sync/sync-saga-message/sync-saga-message-helpers.d.mts.map +1 -1
  36. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs +1 -0
  37. package/dist/sync/sync-saga-message/sync-saga-message-helpers.mjs.map +1 -1
  38. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts +1 -12
  39. package/dist/sync/sync-saga-message/sync-saga-message-types.d.mts.map +1 -1
  40. package/ibgib-foundations.md +20 -2
  41. package/package.json +1 -1
  42. package/src/sync/graft-info/graft-info-constants.mts +4 -0
  43. package/src/sync/graft-info/graft-info-helpers.mts +308 -0
  44. package/src/sync/graft-info/graft-info-helpers.respec.mts +83 -0
  45. package/src/sync/graft-info/graft-info-types.mts +33 -0
  46. package/src/sync/strategies/conflict-optimistic.mts +11 -70
  47. package/src/sync/sync-conflict.respec.mts +171 -35
  48. package/src/sync/sync-constants.mts +1 -4
  49. package/src/sync/sync-peer/sync-peer-innerspace-v1.mts +85 -12
  50. package/src/sync/sync-saga-coordinator.mts +569 -338
  51. package/src/sync/sync-saga-message/sync-saga-message-helpers.mts +2 -0
  52. package/src/sync/sync-saga-message/sync-saga-message-types.mts +0 -11
  53. package/test_output.log +0 -0
  54. package/tmp.md +43 -2
  55. package/dist/sync/merge-info/merge-info-constants.d.mts +0 -2
  56. package/dist/sync/merge-info/merge-info-constants.d.mts.map +0 -1
  57. package/dist/sync/merge-info/merge-info-constants.mjs +0 -2
  58. package/dist/sync/merge-info/merge-info-constants.mjs.map +0 -1
  59. package/dist/sync/merge-info/merge-info-helpers.d.mts +0 -51
  60. package/dist/sync/merge-info/merge-info-helpers.d.mts.map +0 -1
  61. package/dist/sync/merge-info/merge-info-helpers.mjs +0 -92
  62. package/dist/sync/merge-info/merge-info-helpers.mjs.map +0 -1
  63. package/dist/sync/merge-info/merge-info-helpers.respec.d.mts +0 -2
  64. package/dist/sync/merge-info/merge-info-helpers.respec.d.mts.map +0 -1
  65. package/dist/sync/merge-info/merge-info-helpers.respec.mjs +0 -32
  66. package/dist/sync/merge-info/merge-info-helpers.respec.mjs.map +0 -1
  67. package/dist/sync/merge-info/merge-info-types.d.mts +0 -26
  68. package/dist/sync/merge-info/merge-info-types.mjs +0 -2
  69. package/dist/sync/merge-info/merge-info-types.mjs.map +0 -1
  70. package/src/sync/merge-info/merge-info-constants.mts +0 -1
  71. package/src/sync/merge-info/merge-info-helpers.mts +0 -134
  72. package/src/sync/merge-info/merge-info-helpers.respec.mts +0 -41
  73. package/src/sync/merge-info/merge-info-types.mts +0 -28
@@ -31,7 +31,7 @@ import { getDependencyGraph } from "../common/other/graph-helper.mjs";
31
31
  import {
32
32
  SyncSagaMessageData_V1, SyncSagaMessageInitData_V1,
33
33
  SyncSagaMessageAckData_V1, SyncSagaMessageDeltaData_V1,
34
- SyncSagaMessageCommitData_V1, SyncSagaMessageConflictData_V1
34
+ SyncSagaMessageCommitData_V1
35
35
  } from "./sync-saga-message/sync-saga-message-types.mjs";
36
36
  import { getSyncSagaMessageIb } from "./sync-saga-message/sync-saga-message-helpers.mjs";
37
37
  import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-constants.mjs";
@@ -143,7 +143,7 @@ export class SyncSagaCoordinator {
143
143
  const sessionIdentity = useSessionIdentity
144
144
  ? await this.getSessionIdentity({ sagaId, metaspace, tempSpace })
145
145
  : undefined;
146
- 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)`); }
147
147
 
148
148
  // 3. CREATE INITIAL FRAME (Stage.init)
149
149
  const { sagaFrame: initFrame, srcGraph } = await this.createInitFrame({
@@ -335,7 +335,7 @@ export class SyncSagaCoordinator {
335
335
  }
336
336
 
337
337
  // B. Transmit
338
- if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
338
+ // if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
339
339
  updates$.next(requestCtx);
340
340
  const responseCtx = await peer.witness(requestCtx);
341
341
 
@@ -389,6 +389,7 @@ export class SyncSagaCoordinator {
389
389
  await putInSpace({ space: tempSpace, ibGibs: remoteDeps });
390
390
  }
391
391
 
392
+
392
393
  // React (Reducer)
393
394
  // This processes the FRAME we just got from the peer.
394
395
  // i.e., We Sent Init -> Got Ack. This calls handleAckFrame.
@@ -396,7 +397,8 @@ export class SyncSagaCoordinator {
396
397
  const result = await this.handleSagaFrame({
397
398
  sagaIbGib: remoteFrame as SyncIbGib_V1,
398
399
  srcGraph,
399
- 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
400
402
  identity: sessionIdentity,
401
403
  metaspace
402
404
  });
@@ -428,31 +430,65 @@ export class SyncSagaCoordinator {
428
430
  tjpAddrs?: string[],
429
431
  }): Promise<{ [tjp: string]: string | null }> {
430
432
  const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
431
- if (logalot) { console.log(`${lc} starting...`); }
433
+ try {
434
+ if (logalot) { console.log(`${lc} starting... (I: e184f8a7818666febfbbd2d841ed3826)`); }
435
+ console.dir(space)
432
436
 
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
- }
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 {}; }
448
475
 
449
- if (tjps.length === 0) { return {}; }
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
+ }
450
482
 
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)`);
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.`); }
454
491
  }
455
- return res.data.latestAddrsMap;
456
492
  }
457
493
 
458
494
  protected async analyzeTimelines({
@@ -545,12 +581,17 @@ export class SyncSagaCoordinator {
545
581
  initData.knowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
546
582
  });
547
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
+
548
589
  const initStone = await this.createSyncMsgStone({
549
590
  data: initData,
550
591
  space: tempSpace,
551
592
  metaspace
552
593
  });
553
- if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
594
+ // if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
554
595
 
555
596
  const sagaFrame = await this.evolveSyncSagaIbGib({
556
597
  msgStones: [initStone],
@@ -559,7 +600,16 @@ export class SyncSagaCoordinator {
559
600
  metaspace,
560
601
  conflictStrategy,
561
602
  });
562
- if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
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,
609
+ metaspace
610
+ });
611
+
612
+ // if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
563
613
 
564
614
  return { sagaFrame, srcGraph };
565
615
 
@@ -587,14 +637,16 @@ export class SyncSagaCoordinator {
587
637
  async handleSagaFrame({
588
638
  sagaIbGib,
589
639
  srcGraph,
590
- space,
640
+ destSpace,
641
+ tempSpace,
591
642
  identity,
592
643
  identitySecret,
593
644
  metaspace,
594
645
  }: {
595
646
  sagaIbGib: SyncIbGib_V1,
596
647
  srcGraph: { [addr: string]: IbGib_V1 },
597
- space: IbGibSpaceAny,
648
+ destSpace: IbGibSpaceAny,
649
+ tempSpace: IbGibSpaceAny,
598
650
  identity?: KeystoneIbGib_V1,
599
651
  identitySecret?: string,
600
652
  metaspace: MetaspaceService,
@@ -607,25 +659,22 @@ export class SyncSagaCoordinator {
607
659
  if (logalot) { console.log(`${lc} sagaIbGib: ${pretty(sagaIbGib)} (I: 1b99d87d262e9d18d8a607a80b1a0126)`); }
608
660
 
609
661
  // Get Stage from Stone (or Frame for Init fallback)
610
- const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
662
+ const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
611
663
 
612
664
  if (logalot) { console.log(`${lc} handling frame stage: ${stage}`); }
613
665
 
614
666
  switch (stage) {
615
667
  case SyncStage.init:
616
- return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, space, identity, identitySecret });
668
+ return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, destSpace, tempSpace, identity, identitySecret });
617
669
 
618
670
  case SyncStage.ack:
619
- return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, space, identity });
671
+ return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity });
620
672
 
621
673
  case SyncStage.delta:
622
- return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, space, identity, });
674
+ return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity, });
623
675
 
624
676
  case SyncStage.commit:
625
- return await this.handleCommitFrame({ sagaIbGib, metaspace, space, identity, });
626
-
627
- case SyncStage.conflict:
628
- return await this.handleConflictFrame({ sagaIbGib, metaspace, space, });
677
+ return await this.handleCommitFrame({ sagaIbGib, metaspace, destSpace, tempSpace, identity, });
629
678
 
630
679
  default:
631
680
  throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
@@ -655,19 +704,22 @@ export class SyncSagaCoordinator {
655
704
  protected async handleInitFrame({
656
705
  sagaIbGib,
657
706
  messageData,
658
- space,
707
+ destSpace,
708
+ tempSpace,
659
709
  metaspace,
660
710
  identity,
661
711
  identitySecret,
662
712
  }: {
663
713
  sagaIbGib: SyncIbGib_V1,
664
714
  messageData: any,
665
- space: IbGibSpaceAny,
715
+ destSpace: IbGibSpaceAny,
716
+ tempSpace: IbGibSpaceAny,
666
717
  metaspace: MetaspaceService,
667
718
  identity?: KeystoneIbGib_V1,
668
719
  identitySecret?: string,
669
720
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
670
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]'})`);
671
723
  if (logalot) { console.log(`${lc} starting...`); }
672
724
 
673
725
  // Extract Init Data
@@ -675,7 +727,7 @@ export class SyncSagaCoordinator {
675
727
  if (initData.stage !== SyncStage.init) {
676
728
  throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
677
729
  }
678
- if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
730
+ // if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
679
731
  if (!initData || !initData.knowledgeVector) {
680
732
  throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
681
733
  }
@@ -694,7 +746,7 @@ export class SyncSagaCoordinator {
694
746
  if (stones.length > 0) {
695
747
  if (logalot) { console.log(`${lc} processing stones: ${stones.length}`); }
696
748
  // Check if we have these stones
697
- const resStones = await getFromSpace({ space, addrs: stones });
749
+ const resStones = await getFromSpace({ space: destSpace, addrs: stones });
698
750
  const addrsNotFound = resStones.rawResultIbGib?.data?.addrsNotFound;
699
751
  if (addrsNotFound && addrsNotFound.length > 0) {
700
752
  if (logalot) { console.log(`${lc} stones missing (requesting): ${addrsNotFound.length}`); }
@@ -709,6 +761,7 @@ export class SyncSagaCoordinator {
709
761
  const remoteKV = initData.knowledgeVector;
710
762
  if (logalot) { console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`); }
711
763
  const remoteTjps = Object.keys(remoteKV);
764
+ console.log(`${lc} [TEST DEBUG] remoteTjps: ${JSON.stringify(remoteTjps)}`);
712
765
  if (logalot) { console.log(`${lc} remoteTjps: ${pretty(remoteTjps)} (I: 86ea4c53db0dc184c8b253386c402126)`); }
713
766
 
714
767
  // 1. Get Local Latest Addrs for all TJPs
@@ -716,12 +769,13 @@ export class SyncSagaCoordinator {
716
769
  if (remoteTjps.length > 0) {
717
770
  // Batch get latest addrs for the TJPs
718
771
  const resGetLatestAddrs = await getLatestAddrs({
719
- space,
772
+ space: destSpace,
720
773
  tjpAddrs: remoteTjps,
721
774
  });
722
775
  if (!resGetLatestAddrs.data) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data falsy? (E: b180d813c088042b38e1e02e06a16926)`); }
723
776
  if (!resGetLatestAddrs.data.latestAddrsMap) { throw new Error(`(UNEXPECTED) resGetLatestAddrs.data.latestAddrsMap falsy? (E: 16bc386dd51d0ff53a49620b1e641826)`); }
724
777
  localKV = resGetLatestAddrs.data.latestAddrsMap;
778
+ console.log(`${lc} [TEST DEBUG] localKV: ${JSON.stringify(localKV)}`);
725
779
  if (logalot) { console.log(`${lc} localKV: ${pretty(localKV)} (I: 980975642cbccd8018cf0cd808d30826)`); }
726
780
  }
727
781
 
@@ -734,24 +788,28 @@ export class SyncSagaCoordinator {
734
788
 
735
789
  if (!localAddr) {
736
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}`);
737
792
  deltaReqAddrs.push(remoteAddr);
738
793
  continue;
739
794
  }
740
795
 
741
796
  if (localAddr === remoteAddr) {
742
797
  // Synced
798
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Synced (localAddr === remoteAddr)`);
743
799
  continue;
744
800
  }
801
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: localAddr=${localAddr}, remoteAddr=${remoteAddr} - checking for divergence...`);
745
802
 
746
803
  // Check if Remote is in Local's PAST (Local is Ahead -> Push Offer)
747
804
  // (Sender has older version, Receiver has newer) -> Receiver Offers Push
748
805
  const isRemoteInPast = await isPastFrame({
749
806
  olderAddr: remoteAddr,
750
807
  newerAddr: localAddr,
751
- space,
808
+ space: destSpace,
752
809
  });
753
810
 
754
811
  if (isRemoteInPast) {
812
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Remote is in past - offering push`);
755
813
  pushOfferAddrs.push(localAddr);
756
814
  } else {
757
815
  // Remote is not in our past.
@@ -761,14 +819,16 @@ export class SyncSagaCoordinator {
761
819
  const isLocalInPast = await isPastFrame({
762
820
  olderAddr: localAddr,
763
821
  newerAddr: remoteAddr,
764
- space,
822
+ space: destSpace,
765
823
  });
766
824
 
767
825
  if (isLocalInPast) {
768
826
  // Fast-Forward: We update to remote's tip.
827
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Local is in past - requesting delta`);
769
828
  deltaReqAddrs.push(remoteAddr);
770
829
  } else {
771
830
  // DIVERGENCE: Both have changes the other doesn't know about.
831
+ console.log(`${lc} [TEST DEBUG] TJP ${tjp}: DIVERGENCE DETECTED! conflictStrategy=${conflictStrategy}`);
772
832
 
773
833
  if (conflictStrategy === 'abort') {
774
834
  // Abort Strategy: We will treat this as terminal.
@@ -795,7 +855,7 @@ export class SyncSagaCoordinator {
795
855
 
796
856
  // We need the ACTUAL object to get the past.
797
857
  // We have localAddr.
798
- const resLocalTip = await getFromSpace({ space, addr: localAddr! });
858
+ const resLocalTip = await getFromSpace({ space: destSpace, addr: localAddr! });
799
859
  const localTip = resLocalTip.ibGibs?.[0];
800
860
  if (!localTip) { throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`); }
801
861
 
@@ -842,7 +902,7 @@ export class SyncSagaCoordinator {
842
902
  // 2. Add Push Offers (Missing in Local)
843
903
  // Check if we have them. If not, ask for them.
844
904
  for (const addr of pushOfferAddrs) {
845
- const hasIt = await getFromSpace({ addr, space });
905
+ const hasIt = await getFromSpace({ addr, space: destSpace });
846
906
  if (!hasIt.success || !hasIt.ibGibs || hasIt.ibGibs.length === 0) {
847
907
  // If we don't have it, we put it in `deltaReqAddrs` of the Ack.
848
908
  deltaReqAddrs.push(addr);
@@ -861,7 +921,7 @@ export class SyncSagaCoordinator {
861
921
  for (const tjp of remoteTjps) {
862
922
  const localAddr = localKV[tjp];
863
923
  if (localAddr) {
864
- const res = await getFromSpace({ addr: localAddr, space });
924
+ const res = await getFromSpace({ addr: localAddr, space: destSpace });
865
925
  if (res.success && res.ibGibs?.[0]) {
866
926
  const ibGib = res.ibGibs[0];
867
927
  const realTjp = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib }); // Should match `tjp` if normalized
@@ -879,7 +939,7 @@ export class SyncSagaCoordinator {
879
939
  // We can't really know if it's covered easily without resolving.
880
940
  // But if we don't have it (requesting), we won't find it here anyway.
881
941
  // If we DO have it (push offer), we might find it.
882
- const res = await getFromSpace({ addr, space });
942
+ const res = await getFromSpace({ addr, space: destSpace });
883
943
  if (res.success && res.ibGibs?.[0]) {
884
944
  const ibGib = res.ibGibs[0];
885
945
  const tjpAddr = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib });
@@ -918,11 +978,12 @@ export class SyncSagaCoordinator {
918
978
  deltaReqAddrs,
919
979
  pushOfferAddrs,
920
980
  knowledgeVector,
981
+ conflicts: conflicts.length > 0 ? conflicts : undefined, // Include conflicts if any detected
921
982
  };
922
983
 
923
984
  const ackStone = await this.createSyncMsgStone({
924
985
  data: ackData,
925
- space,
986
+ space: tempSpace,
926
987
  metaspace,
927
988
  });
928
989
  if (logalot) { console.log(`${lc} ackStone created: ${pretty(ackStone)} (I: 313708132dd53ff946befb7833657826)`); }
@@ -932,11 +993,14 @@ export class SyncSagaCoordinator {
932
993
  prevSagaIbGib: sagaIbGib,
933
994
  msgStones: [ackStone],
934
995
  identity,
935
- space,
996
+ space: tempSpace,
936
997
  metaspace,
937
998
  });
938
999
 
939
- 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)`); }
940
1004
 
941
1005
  return { frame: ackFrame };
942
1006
  }
@@ -956,280 +1020,364 @@ export class SyncSagaCoordinator {
956
1020
  protected async handleAckFrame({
957
1021
  sagaIbGib,
958
1022
  srcGraph,
959
- space,
1023
+ destSpace,
1024
+ tempSpace,
960
1025
  metaspace,
961
1026
  identity,
962
1027
  }: {
963
1028
  sagaIbGib: SyncIbGib_V1,
964
1029
  srcGraph: { [addr: string]: IbGib_V1 },
965
- space: IbGibSpaceAny,
1030
+ destSpace: IbGibSpaceAny,
1031
+ tempSpace: IbGibSpaceAny,
966
1032
  metaspace: MetaspaceService,
967
1033
  identity?: KeystoneIbGib_V1,
968
1034
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
969
1035
  const lc = `${this.lc}[${this.handleAckFrame.name}]`;
970
- if (logalot) { console.log(`${lc} starting...`); }
1036
+ try {
1037
+ if (logalot) { console.log(`${lc} starting... (I: 605b6860e898267a5b50c6d85704be26)`); }
971
1038
 
972
- const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
973
- const ackData = messageData as SyncSagaMessageAckData_V1;
1039
+ const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
1040
+ const ackData = messageData as SyncSagaMessageAckData_V1;
974
1041
 
975
- if (!ackData) {
976
- throw new Error(`${lc} ackData falsy (E: 3b8415edc876084c88a25b98e2d55826)`);
977
- }
978
- if (ackData.stage !== SyncStage.ack) {
979
- throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
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
- }
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)`); }
992
1049
 
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
- }
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)}`);
1119
1055
  }
1120
- }
1121
1056
 
1122
- // 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
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)`);
1063
+ }
1123
1064
 
1124
- const deltaReqAddrs = ackData.deltaReqAddrs || [];
1125
- const pushOfferAddrs = ackData.pushOfferAddrs || [];
1065
+ const optimisticConflicts = conflicts.filter(c => !c.terminal);
1066
+ const mergeDeltaReqs: string[] = []; // Additional requests for merging
1126
1067
 
1127
- // 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
1128
- const pullReqAddrs: string[] = [];
1129
- for (const addr of pushOfferAddrs) {
1130
- const existing = srcGraph[addr] || (await getFromSpace({ addr, space })).ibGibs?.[0];
1131
- if (!existing) {
1132
- pullReqAddrs.push(addr);
1133
- }
1134
- }
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.
1135
1076
 
1136
- // 2. Process Delta Requests (Push Payload)
1137
- // [NEW] Smart Diff: Use knowledgeVector to skip dependencies
1138
- const skipAddrs = new Set<string>();
1139
- if (ackData.knowledgeVector) {
1140
- Object.values(ackData.knowledgeVector).forEach(addrs => {
1141
- addrs.forEach(a => skipAddrs.add(a));
1142
- });
1143
- }
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.
1144
1080
 
1145
- const payloadIbGibs: IbGib_V1[] = [];
1146
- // Gather all tips to sync first
1147
- const tipsToSync: IbGib_V1[] = [];
1148
- for (const addr of deltaReqAddrs) {
1149
- let ibGib = srcGraph[addr];
1150
- if (!ibGib) {
1151
- const res = await getFromSpace({ addr, space });
1152
- if (res.ibGibs && res.ibGibs.length > 0) {
1153
- ibGib = res.ibGibs[0];
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
+ }
1190
+
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
+ }
1154
1250
  }
1155
- }
1156
- if (ibGib) {
1157
- tipsToSync.push(ibGib);
1251
+
1252
+ console.log(`${lc} [CONFLICT DEBUG] Finished processing ${optimisticConflicts.length} conflicts. mergeDeltaReqs: ${mergeDeltaReqs.length}`);
1158
1253
  } else {
1159
- throw new Error(`${lc} Requested addr not found: ${addr} (E: d41d59cff4a887f6414c3e92eabd8e26)`);
1254
+ console.log(`${lc} [CONFLICT DEBUG] No optimistic conflicts to process`);
1160
1255
  }
1161
- }
1162
1256
 
1163
- // Calculate Dependency Graph for ALL tips, effectively utilizing common history
1164
- // Pass skipAddrs to `getDependencyGraph` or gather manually.
1165
- // `getDependencyGraph` takes a single ibGib.
1166
- // We can optimize by doing it for each tip and unioning the result?
1167
- // Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
1168
- // We will loop.
1169
-
1170
- const allDepsSet = new Set<string>();
1171
-
1172
- for (const tip of tipsToSync) {
1173
- // Always include the tip itself
1174
- const tipAddr = getIbGibAddr({ ibGib: tip });
1175
- // Only process if not skipped (though deltaReq implies they barely just asked for it)
1176
- // But detailed deps might be skipped.
1177
-
1178
- // Get Graph with Skips
1179
- // Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
1180
- const deps = await getDependencyGraph({
1181
- ibGib: tip,
1182
- space,
1183
- skipAddrs: Array.from(skipAddrs)
1184
- });
1257
+ // 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
1185
1258
 
1186
- // [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
1187
- let tipIncluded = false;
1259
+ const deltaReqAddrs = ackData.deltaReqAddrs || [];
1260
+ const pushOfferAddrs = ackData.pushOfferAddrs || [];
1188
1261
 
1189
- if (deps) {
1190
- Object.values(deps).forEach(d => {
1191
- const dAddr = getIbGibAddr({ ibGib: d });
1192
- if (!allDepsSet.has(dAddr)) {
1193
- allDepsSet.add(dAddr);
1194
- payloadIbGibs.push(d);
1195
- }
1196
- 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));
1197
1277
  });
1198
1278
  }
1199
1279
 
1200
- if (!tipIncluded && !skipAddrs.has(tipAddr)) {
1201
- if (logalot) { console.log(`${lc} Tip not in deps, adding explicitly: ${tipAddr}`); }
1202
- if (!allDepsSet.has(tipAddr)) {
1203
- allDepsSet.add(tipAddr);
1204
- 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)`);
1205
1295
  }
1206
1296
  }
1207
- }
1208
1297
 
1209
- // 3. Create Delta Frame
1210
- const sagaId = ackData.sagaId;
1211
- const deltaData: SyncSagaMessageDeltaData_V1 = {
1212
- sagaId: sagaIbGib.data!.uuid,
1213
- stage: SyncStage.delta,
1214
- payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
1215
- requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
1216
- };
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
+ });
1217
1320
 
1218
- const deltaStone = await this.createSyncMsgStone({
1219
- data: deltaData,
1220
- space,
1221
- metaspace,
1222
- });
1321
+ // [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
1322
+ let tipIncluded = false;
1223
1323
 
1224
- const deltaFrame = await this.evolveSyncSagaIbGib({
1225
- prevSagaIbGib: sagaIbGib,
1226
- msgStones: [deltaStone],
1227
- identity,
1228
- space,
1229
- metaspace,
1230
- });
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
+ }
1334
+
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)}`); }
1231
1373
 
1232
- return { frame: deltaFrame, payloadIbGibs };
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
+ }
1233
1381
  }
1234
1382
 
1235
1383
  /**
@@ -1245,20 +1393,22 @@ export class SyncSagaCoordinator {
1245
1393
  protected async handleDeltaFrame({
1246
1394
  sagaIbGib,
1247
1395
  srcGraph,
1248
- space,
1396
+ destSpace,
1397
+ tempSpace,
1249
1398
  metaspace,
1250
1399
  identity,
1251
1400
  }: {
1252
1401
  sagaIbGib: SyncIbGib_V1,
1253
1402
  srcGraph: { [addr: string]: IbGib_V1 },
1254
- space: IbGibSpaceAny,
1403
+ destSpace: IbGibSpaceAny,
1404
+ tempSpace: IbGibSpaceAny,
1255
1405
  metaspace: MetaspaceService,
1256
1406
  identity?: KeystoneIbGib_V1,
1257
1407
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[], receivedPayloadIbGibs?: IbGib_V1[] } | null> {
1258
1408
  const lc = `${this.lc}[${this.handleDeltaFrame.name}]`;
1259
1409
  if (logalot) { console.log(`${lc} starting...`); }
1260
1410
 
1261
- const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
1411
+ const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
1262
1412
  const deltaData = messageData as SyncSagaMessageDeltaData_V1;
1263
1413
 
1264
1414
  if (!deltaData) {
@@ -1269,6 +1419,8 @@ export class SyncSagaCoordinator {
1269
1419
  }
1270
1420
  if (logalot) { console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`); }
1271
1421
 
1422
+ console.log(`${lc} [CONFLICT DEBUG] deltaData.payloadAddrs count: ${deltaData.payloadAddrs?.length || 0}`);
1423
+
1272
1424
  const payloadAddrs = deltaData.payloadAddrs || [];
1273
1425
  const peerRequests = deltaData.requests || [];
1274
1426
  const peerProposesCommit = deltaData.proposeCommit || false;
@@ -1284,7 +1436,7 @@ export class SyncSagaCoordinator {
1284
1436
 
1285
1437
  const res = await getFromSpace({
1286
1438
  addrs: payloadAddrs,
1287
- space,
1439
+ space: tempSpace, // Incoming data is in tempSpace
1288
1440
  });
1289
1441
  if (res.ibGibs) {
1290
1442
  receivedPayloadIbGibs.push(...res.ibGibs);
@@ -1295,49 +1447,89 @@ export class SyncSagaCoordinator {
1295
1447
  }
1296
1448
  }
1297
1449
 
1298
- // 2. Fulfill Peer Requests (Outgoing Payload)
1450
+ // 2. Fulfill Peer Requests (Outgoing Payload with Delta Dependencies)
1299
1451
  const outgoingPayload: IbGib_V1[] = [];
1452
+ const outgoingAddrsSet = new Set<string>(); // Track what we've added
1453
+
1454
+ console.log(`${lc} [CONFLICT DEBUG] Fulfilling ${peerRequests.length} peer requests`);
1455
+
1300
1456
  for (const addr of peerRequests) {
1457
+ // Get the requested ibGib
1301
1458
  let ibGib = srcGraph[addr];
1302
1459
  if (!ibGib) {
1303
- const res = await getFromSpace({ addr, space });
1460
+ const res = await getFromSpace({ addr, space: destSpace }); // Query from destSpace
1304
1461
  if (res.ibGibs && res.ibGibs.length > 0) {
1305
1462
  ibGib = res.ibGibs[0];
1306
1463
  }
1307
1464
  }
1465
+
1308
1466
  if (ibGib) {
1309
- 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
+ }
1310
1494
  } else {
1311
1495
  console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
1312
1496
  }
1313
1497
  }
1314
1498
 
1499
+ console.log(`${lc} [CONFLICT DEBUG] Outgoing payload size (with deps): ${outgoingPayload.length}`);
1500
+
1501
+
1315
1502
  // 3. Execute Merges (If applicable)
1316
1503
  // Check if we have pending conflicts that we CAN resolve now that we have data.
1317
1504
  // We look at the Saga History (Ack Frame) to find conflicts.
1318
1505
  // Optimization: Do this only if we received payloads.
1319
1506
  const mergeResultIbGibs: IbGib_V1[] = [];
1320
1507
 
1508
+ console.log(`${lc} [CONFLICT DEBUG] Checking for merge. receivedPayloadIbGibs.length: ${receivedPayloadIbGibs.length}`);
1509
+
1321
1510
  if (receivedPayloadIbGibs.length > 0) {
1511
+ console.log(`${lc} [TEST DEBUG] Received Payloads (${receivedPayloadIbGibs.length}). Checking for conflicts/merges...`);
1322
1512
  // Find the Ack frame in history to get conflicts
1323
1513
  // Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
1324
1514
  // V1 timelines carry full history in `past`.
1325
1515
  const pastAddrs = sagaIbGib.rel8ns?.past || [];
1516
+ console.log(`${lc} [TEST DEBUG] pastAddrs count: ${pastAddrs.length}`);
1326
1517
  let ackData: SyncSagaMessageAckData_V1 | undefined;
1327
1518
 
1328
1519
  if (pastAddrs.length > 0) {
1329
1520
  // Batch fetch all past frames
1330
- const resPast = await getFromSpace({ addrs: pastAddrs, space });
1521
+ const resPast = await getFromSpace({ addrs: pastAddrs, space: tempSpace });
1331
1522
  if (resPast.success && resPast.ibGibs) {
1332
1523
  // Iterate backwards (most recent first) to find the latest Ack
1333
1524
  for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
1334
1525
  const pastFrame = resPast.ibGibs[i];
1335
1526
  const messageStone = await getSyncSagaMessageFromFrame({
1336
1527
  frameIbGib: pastFrame,
1337
- space
1528
+ space: tempSpace
1338
1529
  });
1339
1530
  if (messageStone?.data?.stage === SyncStage.ack) {
1340
1531
  ackData = messageStone.data as SyncSagaMessageAckData_V1;
1532
+ console.log(`${lc} [TEST DEBUG] Found Ack Frame. Conflicts: ${ackData.conflicts?.length || 0}`);
1341
1533
  break;
1342
1534
  }
1343
1535
  }
@@ -1356,7 +1548,9 @@ export class SyncSagaCoordinator {
1356
1548
  // We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
1357
1549
 
1358
1550
  // Check if we have receiverTip in space
1359
- const resRecTip = await getFromSpace({ addr: receiverTip, 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]}`);
1360
1554
  if (resRecTip.success && resRecTip.ibGibs?.[0]) {
1361
1555
  // We have the tip!
1362
1556
  // Do we have the full history?
@@ -1366,12 +1560,13 @@ export class SyncSagaCoordinator {
1366
1560
  // Perform Merge!
1367
1561
  try {
1368
1562
  const mergeResult = await mergeDivergentTimelines({
1369
- tipA: (await getFromSpace({ addr: senderTip, space })).ibGibs![0], // Our tip
1370
- tipB: resRecTip.ibGibs[0], // Their tip
1371
- space,
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
1372
1566
  metaspace,
1373
1567
  });
1374
1568
  if (mergeResult) {
1569
+ console.log(`${lc} [TEST DEBUG] Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
1375
1570
  if (logalot) { console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`); }
1376
1571
  mergeResultIbGibs.push(mergeResult);
1377
1572
  outgoingPayload.push(mergeResult); // Send result to peer
@@ -1419,7 +1614,7 @@ export class SyncSagaCoordinator {
1419
1614
 
1420
1615
  const deltaStone = await this.createSyncMsgStone({
1421
1616
  data: responseDeltaData,
1422
- space,
1617
+ space: tempSpace,
1423
1618
  metaspace
1424
1619
  });
1425
1620
 
@@ -1427,10 +1622,13 @@ export class SyncSagaCoordinator {
1427
1622
  prevSagaIbGib: sagaIbGib,
1428
1623
  msgStones: [deltaStone],
1429
1624
  identity,
1430
- space,
1625
+ space: tempSpace,
1431
1626
  metaspace
1432
1627
  });
1433
1628
 
1629
+ // IMMEDIATELY persist to both spaces for audit trail
1630
+ await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
1631
+
1434
1632
  return { frame: deltaFrame, payloadIbGibs: outgoingPayload, receivedPayloadIbGibs };
1435
1633
 
1436
1634
  } else {
@@ -1446,7 +1644,7 @@ export class SyncSagaCoordinator {
1446
1644
 
1447
1645
  const commitStone = await this.createSyncMsgStone({
1448
1646
  data: commitData,
1449
- space,
1647
+ space: tempSpace,
1450
1648
  metaspace
1451
1649
  });
1452
1650
 
@@ -1454,10 +1652,13 @@ export class SyncSagaCoordinator {
1454
1652
  prevSagaIbGib: sagaIbGib,
1455
1653
  msgStones: [commitStone],
1456
1654
  identity,
1457
- space,
1655
+ space: tempSpace,
1458
1656
  metaspace
1459
1657
  });
1460
1658
 
1659
+ // IMMEDIATELY persist to both spaces for audit trail
1660
+ await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
1661
+
1461
1662
  return { frame: commitFrame, receivedPayloadIbGibs };
1462
1663
 
1463
1664
  } else {
@@ -1473,7 +1674,7 @@ export class SyncSagaCoordinator {
1473
1674
 
1474
1675
  const deltaStone = await this.createSyncMsgStone({
1475
1676
  data: responseDeltaData,
1476
- space,
1677
+ space: tempSpace,
1477
1678
  metaspace
1478
1679
  });
1479
1680
 
@@ -1481,10 +1682,13 @@ export class SyncSagaCoordinator {
1481
1682
  prevSagaIbGib: sagaIbGib,
1482
1683
  msgStones: [deltaStone],
1483
1684
  identity,
1484
- space,
1685
+ space: tempSpace,
1485
1686
  metaspace
1486
1687
  });
1487
1688
 
1689
+ // IMMEDIATELY persist to both spaces for audit trail
1690
+ await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
1691
+
1488
1692
  // Check if PEER proposed commit
1489
1693
  if (deltaData.proposeCommit) {
1490
1694
  if (logalot) { console.log(`${lc} Peer proposed commit. Accepting & Committing.`); }
@@ -1499,7 +1703,7 @@ export class SyncSagaCoordinator {
1499
1703
 
1500
1704
  const commitStone = await this.createSyncMsgStone({
1501
1705
  data: commitData,
1502
- space,
1706
+ space: tempSpace,
1503
1707
  metaspace
1504
1708
  });
1505
1709
 
@@ -1507,10 +1711,13 @@ export class SyncSagaCoordinator {
1507
1711
  prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
1508
1712
  msgStones: [commitStone],
1509
1713
  identity,
1510
- space,
1714
+ space: tempSpace,
1511
1715
  metaspace
1512
1716
  });
1513
1717
 
1718
+ // IMMEDIATELY persist to both spaces for audit trail
1719
+ await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
1720
+
1514
1721
  return { frame: commitFrame, receivedPayloadIbGibs };
1515
1722
  }
1516
1723
 
@@ -1522,11 +1729,14 @@ export class SyncSagaCoordinator {
1522
1729
 
1523
1730
  protected async handleCommitFrame({
1524
1731
  sagaIbGib,
1525
- space,
1732
+ destSpace,
1733
+ tempSpace,
1526
1734
  metaspace,
1735
+ identity,
1527
1736
  }: {
1528
1737
  sagaIbGib: SyncIbGib_V1,
1529
- space: IbGibSpaceAny,
1738
+ destSpace: IbGibSpaceAny,
1739
+ tempSpace: IbGibSpaceAny,
1530
1740
  metaspace: MetaspaceService,
1531
1741
  identity?: KeystoneIbGib_V1,
1532
1742
  }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
@@ -1547,29 +1757,6 @@ export class SyncSagaCoordinator {
1547
1757
  return null;
1548
1758
  }
1549
1759
 
1550
- protected async handleConflictFrame({
1551
- sagaIbGib,
1552
- metaspace,
1553
- space,
1554
- }: {
1555
- sagaIbGib: SyncIbGib_V1,
1556
- metaspace: MetaspaceService,
1557
- space: IbGibSpaceAny,
1558
- }): Promise<{ frame: SyncIbGib_V1, payloadIbGibs?: IbGib_V1[] } | null> {
1559
- const lc = `${this.lc}[${this.handleConflictFrame.name}]`;
1560
- const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space });
1561
- const conflictData = messageData as SyncSagaMessageConflictData_V1;
1562
-
1563
- if (logalot) { console.log(`${lc} Conflict received. Strategy: ${conflictData?.conflictStrategy}. Terminal: ${conflictData?.isTerminal}`); }
1564
-
1565
- if (conflictData?.isTerminal) {
1566
- throw new Error(`${lc} Saga aborted due to conflicts: ${JSON.stringify(conflictData.conflicts)} (E: b08d1f2a3c4e5d6f7a8b9c0d1e2f3a4b)`);
1567
- }
1568
-
1569
- // Non-terminal logic (stub for future)
1570
- return null;
1571
- }
1572
-
1573
1760
  // #endregion Handlers
1574
1761
 
1575
1762
  protected async createSyncMsgStone<TStoneData extends SyncSagaMessageData_V1>({
@@ -1605,6 +1792,50 @@ export class SyncSagaCoordinator {
1605
1792
  }
1606
1793
 
1607
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
+
1608
1839
  /**
1609
1840
  * Evolves the saga timeline with a new frame.
1610
1841
  */