import { extractErrorMsg, getUUID, // so our timestamp in ticks as a string are uniform
pretty, } from "@ibgib/helper-gib/dist/helpers/utils-helper.mjs";
import { getIbGibAddr } from "@ibgib/ts-gib/dist/helper.mjs";
import { splitPerTjpAndOrDna, getTimelinesGroupedByTjp, isIbGib } from "../common/other/ibgib-helper.mjs";
import { Factory_V1 } from "@ibgib/ts-gib/dist/V1/factory.mjs";
import { putInSpace, getLatestAddrs, getFromSpace } from "../witness/space/space-helper.mjs";
import { SyncStage, SYNC_ATOM, SYNC_MSG_REL8N_NAME, SYNC_SAGA_PAYLOAD_ADDRS_DOMAIN } from "./sync-constants.mjs";
import { appendToTimeline, createTimeline } from "../timeline/timeline-api.mjs";
import { SyncMode, } from "./sync-types.mjs";
import { getSyncIb, isPastFrame } from "./sync-helpers.mjs";
import { getSyncSagaDependencyGraph as getSyncSagaDependencyGraphForThisFrameOnly } from "./sync-helpers.mjs";
import { getDependencyGraph } from "../common/other/graph-helper.mjs";
import { getSyncSagaMessageIb } from "./sync-saga-message/sync-saga-message-helpers.mjs";
import { SYNC_SAGA_MSG_ATOM } from "./sync-saga-message/sync-saga-message-constants.mjs";
import { createSyncSagaContext } from "./sync-saga-context/sync-saga-context-helpers.mjs";
import { newupSubject, } from "../common/pubsub/subject/subject-helper.mjs";
import { mergeDivergentTimelines } from "./strategies/conflict-optimistic.mjs";
import { getSyncSagaMessageFromFrame } from "./sync-saga-message/sync-saga-message-helpers.mjs";
import { fnObs } from "../common/pubsub/observer/observer-helper.mjs";
import { SyncPeerInnerspace_V1 } from "./sync-peer/sync-peer-innerspace/sync-peer-innerspace-v1.mjs";
// const logalot = GLOBAL_LOG_A_LOT || true;
const logalot = false;
const logalotControlDomain = true;
const lcControlDomain = '[ControlDomain]';
/**
 * Orchestrates the synchronization process between two spaces (Source and Destination).
 *
 * ## Execution Contexts
 *
 * The methods in this class are designed to be run on specific nodes in the sync topology:
 *
 * *   **Sender/Local Node**: The node *initiating* the sync (calling `sync()`).
 * *   **Receiver/Remote Node**: The node *accepting* the sync request (the Peer).
 *
 * Note that in a peer-to-peer architecture, "Sender" and "Receiver" are roles relative
 * to a specific Saga session, not fixed node identities.
 */
export class SyncSagaCoordinator {
    keystone;
    lc = `[${SyncSagaCoordinator.name}]`;
    constructor(keystone) {
        this.keystone = keystone;
    }
    /**
     * Executes a synchronization saga using the Symmetric Sync Protocol.
     *
     * @remarks
     * **Execution Context**: **Sender (Local)**.
     * This method is the entry point for starting a sync session.
     *
     * @param opts.peer - The remote peer witness to communicate with.
     * @param opts.localSpace - The local space that will be read from and written to.
     * @param opts.metaspace - Service for creating temp spaces and managing ibgibs.
     * @param opts.domainIbGibs - The root ibgibs defining the scope of the sync.
     * @param opts.useSessionIdentity - (Optional) Whether to create an ephemeral session identity. Default: true.
     */
    async sync({ peer, localSpace: _localSpace, source: _source, metaspace, domainIbGibs, conflictStrategy = 'abort', useSessionIdentity = true, }) {
        const lc = `${this.lc}[${this.sync.name}]`;
        if (logalot) {
            console.log(`${lc} starting...`);
        }
        const localSpace = (_source || _localSpace);
        if (!localSpace) {
            throw new Error(`${lc} source (or localSpace) required (E: 8a9b0c1d)`);
        }
        // 1. SETUP SAGA METADATA
        const sagaId = await getUUID();
        // Setup Observable & Promise
        // const updates$ = new Subject_V1<SyncSagaContextIbGib_V1>();
        const updates$ = await newupSubject();
        if (!updates$.next) {
            throw new Error(`(UNEXPECTED) updates$.next falsy? (E: d24cd82184aec130c89a320819b39126)`);
        }
        let resolveDone;
        let rejectDone;
        const done = new Promise((resolve, reject) => {
            resolveDone = resolve;
            rejectDone = reject;
        });
        async function getTempSpaceName() {
            const uuid = await getUUID();
            return `tmp_sync_space_${uuid.substring(0, 8)}`;
        }
        // WORKING CONTEXT (Transactional)
        const tempSpaceName = await getTempSpaceName();
        const tempSpace = await metaspace.createNewLocalSpace({
            opts: {
                allowCancel: false,
                spaceName: tempSpaceName,
                getFnPrompt: metaspace.getFnPrompt,
                logalot
            }
        });
        if (!tempSpace) {
            throw new Error(`Failed to create temp space (E: 8f4e2f3d6c1b4b1a8f4e2f3d6c1b4b1a)`);
        }
        await tempSpace.initialized;
        const cleanup = async () => {
            // In-memory temp space cleanup if needed (e.g. if we used a specialized disposable space)
            // await tempSpace.destroy();
        };
        // Async execution wrapper
        (async () => {
            try {
                // 2. BOOTSTRAP IDENTITY (Session Keystone)
                const sessionIdentity = useSessionIdentity
                    ? await this.getSessionIdentity({ sagaId, metaspace, tempSpace })
                    : undefined;
                // if (logalot) { console.log(`${lc} sessionIdentity: ${sessionIdentity ? pretty(sessionIdentity) : 'undefined'} (I: abc01872800b3a66b819a05898bba826)`); }
                // 3. CREATE INITIAL FRAME (Stage.init)
                const { sagaFrame: initFrame, srcGraph } = await this.createInitFrame({
                    sagaId,
                    sessionIdentity,
                    localSpace,
                    domainIbGibs,
                    tempSpace,
                    metaspace,
                    conflictStrategy
                });
                // 4. EXECUTE SAGA LOOP (FSM)
                // Inject tempSpace into peer so it can pull control payloads to the right place
                // if ('opts' in peer && peer.opts) {
                if (!peer.data) {
                    throw new Error(`(UNEXPECTED) peer.data falsy? (E: 8546a884c82ffb1999e95d9867da2826)`);
                }
                if (peer.data.classname === SyncPeerInnerspace_V1.name) {
                    // (peer as SyncPeerInnerspace_V1).senderTempSpace = tempSpace;
                }
                const syncedIbGibs = await this.executeSagaLoop({
                    initialFrame: initFrame,
                    srcGraph,
                    peer,
                    sessionIdentity,
                    updates$,
                    localSpace,
                    tempSpace,
                    metaspace
                });
                // 5. MERGE RESULT (Optimistic Commit)
                if (syncedIbGibs && syncedIbGibs.length > 0) {
                    if (logalot) {
                        console.log(`${lc} Merging ${syncedIbGibs.length} synced ibGibs to localSpace...`);
                    }
                    await putInSpace({ space: localSpace, ibGibs: syncedIbGibs });
                }
                resolveDone();
                if (!updates$.complete) {
                    throw new Error(`(UNEXPECTED) updates$.complete falsy? (E: d24cd82184aec130c89a320819b39126)`);
                }
                updates$.complete();
            }
            catch (error) {
                console.error(`${lc} ${extractErrorMsg(error)}`);
                rejectDone(error);
                if (!updates$.error) {
                    throw new Error(`(UNEXPECTED) updates$.error falsy? (E: 392ce77c073272e4c398c3e86583ce26)`);
                }
                updates$.error(error);
            }
            finally {
                await cleanup();
            }
        })();
        return {
            sagaId,
            updates$,
            done
        };
    }
    async getSessionIdentity({ sagaId, metaspace, tempSpace, }) {
        const lc = `${this.lc}[${this.getSessionIdentity.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 428392a4ee636b7bd8f7d5d89a87e826)`);
            }
            const config = {
                id: 'default',
                type: 'hash-reveal-v1',
                salt: sagaId,
                behavior: { size: 10, replenish: 'top-up', selectSequentially: 1, selectRandomly: 1, targetBindingChars: 0 },
                algo: 'SHA-256', rounds: 1
            };
            const sessionIdentity = await this.keystone.genesis({
                masterSecret: sagaId,
                configs: [config],
                metaspace,
                space: tempSpace
            });
            return sessionIdentity;
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    /**
     * Drives the FSM loop of the Saga.
     *
     * @remarks
     * **Execution Context**: **Sender (Local)**.
     *
     * This method manages the "Ping Pong" request-response cycle on the Sender.
     * It sends frames via the Peer Witness and processes the responses using `handleSagaFrame`.
     *
     * **Data Transport Note**:
     * Actual ibGib data (payloads) are transported via `SyncSagaContext.rel8ns.payload`.
     * When `handleSagaFrame` returns a `nextPayloadIbGibs` (data to send), this loop injects it into
     * the NEXT request context.
     * When the Peer responds with data (in the response context), it is resolved and put into `tempSpace`.
     */
    async executeSagaLoop({ initialFrame, srcGraph, peer, sessionIdentity, updates$, localSpace, tempSpace, metaspace }) {
        const lc = `${this.lc}[${this.executeSagaLoop.name}]`;
        // The current frame we just generated (e.g., Init or Delta Request)
        let currentFrame = initialFrame;
        // The payload we need to attach to the message (data we are sending)
        let nextDomainIbGibs = [];
        // Accumulator for all data we've successfully pulled from the remote
        const allReceivedIbGibs = [];
        while (currentFrame) {
            // A. Create Context (Request)
            // 1. Calculate Full Dependency Graph (including Ancestors/DNA)
            // TODO: adjust this algorithm to only do the dependencies that the other end doesn't need (diff between tip and LCA)
            // We must do this BEFORE creating the Context so we can list them
            // all in payloadAddrsDomain and payloadAddrsControl.
            // const depsDomainIbGibs: IbGib_V1[] = [];
            // const depsControlIbGibs: IbGib_V1[] = [];
            const payloadIbGibsControl = [];
            const payloadIbGibsDomain = [];
            // A. Payload (Standard Deep Deps)
            for (const nextDomainIbGib of nextDomainIbGibs) {
                let nextDomainIbGibGraph = await getDependencyGraph({ ibGib: nextDomainIbGib, space: localSpace });
                if (!nextDomainIbGibGraph) {
                    nextDomainIbGibGraph = await getDependencyGraph({ ibGib: nextDomainIbGib, space: tempSpace });
                }
                if (nextDomainIbGibGraph) {
                    payloadIbGibsDomain.push(...Object.values(nextDomainIbGibGraph));
                }
                else {
                    throw new Error(`(UNEXPECTED) we couldn't get the graph for a known domain ibgib? nextDomainIbGib addr: ${getIbGibAddr({ ibGib: nextDomainIbGib })} (E: 01b3e4db8768b5b77db72e486f4f7826)`);
                }
            }
            if (logalot) {
                console.log(`${lc} payloadIbGibsDomain count: ${payloadIbGibsDomain.length} (I: 2beda8ca7dc5ac0f48ed9e25e704b826)`);
            }
            // B. Frames (Shallow Sync Deps)
            if (currentFrame) {
                const depsCurrentFrame = await getSyncSagaDependencyGraphForThisFrameOnly({ ibGib: currentFrame, space: tempSpace });
                if (depsCurrentFrame.length > 0) {
                    depsCurrentFrame.forEach(x => payloadIbGibsControl.push(x));
                }
                else {
                    throw new Error(`(UNEXPECTED) couldn't get deps for currentFrame? currentFrame: ${JSON.stringify(currentFrame)} (E: 06344d07adc80d80b809211171444d26)`);
                }
            }
            // 2. Create Context (Envelope)
            const domainPayloadsMap = new Map();
            const sublc = `${lc}[peer.payloadIbGibsDomainReceived$]`;
            let subscription;
            if (peer && peer.payloadIbGibsDomainReceived$) {
                // Subscribe to stream
                subscription = await peer.payloadIbGibsDomainReceived$.subscribe(fnObs({
                    next: async (ibgib) => {
                        if (logalot) {
                            console.log(`${sublc} next fired. (I: 2b4bdf502a38a90ba33d9711e7cb7826)`);
                        }
                        const addr = getIbGibAddr({ ibGib: ibgib });
                        if (logalotControlDomain) {
                            console.log(`${lc}${lcControlDomain} DOMAIN STREAM RECEIVED <- observable: ${addr} (I: d69ee80fcaece272483ec33b2d289826)`);
                        }
                        domainPayloadsMap.set(addr, ibgib);
                    },
                    error: async (e) => {
                        if (isIbGib(e)) {
                            console.error(`${sublc} error fired. error: ${JSON.stringify(e.data)} (E: 01cc08ba05ad99682831174fd7c31a26)`);
                        }
                        else {
                            console.dir(e);
                            console.error(`${sublc} error fired. error: ${extractErrorMsg(e)} (E: 73d3d61464e8e4ce4cd6efd8b9675826)`);
                        }
                    },
                    complete: async () => {
                        if (logalot) {
                            console.log(`${sublc} complete fired. (I: a47218aa9e4433fdb97c068880a45826)`);
                        }
                        await subscription.unsubscribe();
                    },
                }));
            }
            // 2b. Request Context
            const requestCtx = await createSyncSagaContext({
                sagaFrame: currentFrame,
                sessionKeystones: sessionIdentity ? [sessionIdentity] : undefined,
                payloadIbGibsDomain,
            });
            // Log what we're sending
            if (logalotControlDomain) {
                const controlAddrs = payloadIbGibsControl.map(p => getIbGibAddr({ ibGib: p }));
                const domainAddrs = payloadIbGibsDomain.map(p => getIbGibAddr({ ibGib: p }));
                console.log(`${lc}${lcControlDomain} SENDER TRANSMIT -> peer.witness (I: b3c4d5e6f7a8b9c0)`);
                console.log(`${lc}${lcControlDomain}   Context: ${getIbGibAddr({ ibGib: requestCtx })}`);
                console.log(`${lc}${lcControlDomain}   Frame: ${getIbGibAddr({ ibGib: currentFrame })}`);
                console.log(`${lc}${lcControlDomain}   CONTROL Payloads (${controlAddrs.length}): ${controlAddrs.join(', ') || '(none)'}`);
                console.log(`${lc}${lcControlDomain}   DOMAIN Payloads (${domainAddrs.length}): ${domainAddrs.join(', ') || '(none)'}`);
            }
            // Add Context Deps
            // do we need to add requestCtx to the payload ibgibs?
            // if (requestCtx) {
            //     const deps = await getSyncSagaDependencyGraphForThisFrameOnly({ ibGib: requestCtx, space: tempSpace });
            //     if (deps) { deps.forEach(x => payloadIbGibsControl.push(x)); }
            // }
            // 3. Identity (if exists)
            // Identity might be deep? Keystone? Usually self-contained or shallow references.
            if (sessionIdentity) {
                payloadIbGibsControl.push(sessionIdentity);
            }
            // we only put the **CONTROL** payload ibgibs in localSpace. we
            // don't put any domain ibgibs into this durable space until the
            // final commit phase.
            // The requestCtx envelope itself also goes to localSpace so transfer can find it.
            await putInSpace({ space: localSpace, ibGib: requestCtx });
            if (payloadIbGibsControl.length > 0) {
                await putInSpace({ space: localSpace, ibGibs: payloadIbGibsControl });
            }
            // B. Transmit
            // if (logalot) { console.log(`${lc} transmitting... requestCtx: ${pretty(requestCtx)} (I: 8cf20817c66899abdb1e76df50356826)`); }
            updates$.next(requestCtx); // spins off (don't remove this comment!)
            const responseCtx = await peer.witness(requestCtx);
            // C. Handle Response
            if (!responseCtx) {
                if (currentFrame) {
                    // Check for Commit (Peer silence expected)
                    const msg = await getSyncSagaMessageFromFrame({ frameIbGib: currentFrame, space: localSpace });
                    if (msg?.data?.stage === SyncStage.commit) {
                        if (logalot) {
                            console.log(`${lc} Sender sent Commit. Peer returned no response. Saga Complete.`);
                        }
                        currentFrame = null;
                        break;
                    }
                    else {
                        throw new Error(`(UNEXPECTED) responseCtx falsy and currentFrame truthy, but we're not in the commit stage? This may be expected ultimately, but atow I am not seeing this as being expected. (E: cc34498962bd370deeff351fac939f26)`);
                    }
                }
                else {
                    throw new Error(`(UNEXPECTED) no response and currentFrame falsy? (E: 8d1085ea2f28cfc3f9c922649864a826)`);
                }
            }
            // ---------------------------------------------------------------------
            // 2d. HANDLE RESPONSE
            // ---------------------------------------------------------------------
            if (!responseCtx.data) {
                throw new Error(`(UNEXPECTED) responseCtx.data falsy? (E: a969992bae53ab18a827ec58aec15826)`);
            }
            updates$.next(responseCtx); // spins off -- don't remove this comment!
            // Extract expected domain addresses from response context
            const responsePayloadAddrsDomain = responseCtx.data[SYNC_SAGA_PAYLOAD_ADDRS_DOMAIN] || [];
            // TODO: check if we are validating the responseCtx in sync peer. I'm thinking that we must, because we can't start the domain ibgibs until we know the response's control ibgibs are valid.
            // Poll for them if needed
            if (responsePayloadAddrsDomain.length > 0) {
                await this.pollForDomainPayloads({
                    expectedAddrs: responsePayloadAddrsDomain,
                    domainPayloadsMap,
                    tempSpace,
                });
            }
            // Extract Response Frame
            const responseFrameAddr = responseCtx.rel8ns?.sagaFrame?.[0];
            if (!responseFrameAddr) {
                throw new Error(`${lc} Peer response missing sagaFrame (E: 83a0)`);
            }
            // Log what we received back
            if (logalotControlDomain) {
                const responseControlAddrs = responseCtx.data?.['@payloadAddrsControl'] || [];
                console.log(`${lc}${lcControlDomain} SENDER RECEIVED <- peer.witness (I: c4d5e6f7a8b9c0d1)`);
                console.log(`${lc}${lcControlDomain}   Response Context: ${getIbGibAddr({ ibGib: responseCtx })}`);
                console.log(`${lc}${lcControlDomain}   Response Frame: ${responseFrameAddr}`);
                console.log(`${lc}${lcControlDomain}   CONTROL Payloads (${responseControlAddrs.length}): ${responseControlAddrs.join(', ') || '(none)'}`);
                console.log(`${lc}${lcControlDomain}   DOMAIN Payloads (${responsePayloadAddrsDomain.length}): ${responsePayloadAddrsDomain.join(', ') || '(none)'}`);
            }
            // Get response frame from localSpace (SyncPeer puts it there)
            let resResponseFrame = await getFromSpace({ space: localSpace, addr: responseFrameAddr });
            if (!resResponseFrame.success || !resResponseFrame.ibGibs?.length) {
                // Fallback to tempSpace
                resResponseFrame = await getFromSpace({ space: tempSpace, addr: responseFrameAddr });
            }
            const responseFrame = resResponseFrame.ibGibs?.[0];
            if (!responseFrame)
                throw new Error(`${lc} Response frame not found (E: 7c2a)`);
            // Handle Response Frame
            const handleResult = await this.handleSagaFrame({
                sagaIbGib: responseFrame,
                srcGraph: {},
                destSpace: localSpace,
                tempSpace,
                metaspace,
                domainPayloadsMap,
                expectedDomainAddrs: responsePayloadAddrsDomain,
            });
            if (!handleResult) {
                if (logalot) {
                    console.log(`${lc} Handler returned null (Saga End).`);
                }
                break;
            }
            currentFrame = handleResult.frame;
            // Collect next DOMAIN payloads for the NEXT request
            // Control payloads are handled separately by ensureSagaFrameInBothSpaces
            nextDomainIbGibs = [...(handleResult.payloadIbGibsDomain || [])];
            // Log handler output for next iteration
            if (logalotControlDomain) {
                const handlerControlAddrs = (handleResult.payloadIbGibsControl || []).map(p => getIbGibAddr({ ibGib: p }));
                const handlerDomainAddrs = nextDomainIbGibs.map(p => getIbGibAddr({ ibGib: p }));
                console.log(`${lc}${lcControlDomain} HANDLER RESULT -> next iteration (I: d5e6f7a8b9c0d1e2)`);
                console.log(`${lc}${lcControlDomain}   Next Frame: ${getIbGibAddr({ ibGib: currentFrame })}`);
                console.log(`${lc}${lcControlDomain}   CONTROL for next (${handlerControlAddrs.length}): ${handlerControlAddrs.join(', ') || '(none)'} (saved via ensureSagaFrameInBothSpaces)`);
                console.log(`${lc}${lcControlDomain}   DOMAIN for next (${handlerDomainAddrs.length}): ${handlerDomainAddrs.join(', ') || '(none)'}`);
            }
            // Note: currentFrame is automatically added to dependencies at start of next loop via B. Frames logic.
        }
        return allReceivedIbGibs;
    }
    /**
     * Helper to get Knowledge Vector for specific domain ibGibs or TJPs.
     * Useful for testing and external validation.
     */
    async getKnowledgeVector({ space, metaspace, domainIbGibs, tjpAddrs, }) {
        const lc = `${this.lc}[${this.getKnowledgeVector.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: e184f8a7818666febfbbd2d841ed3826)`);
            }
            // console.dir(space);
            if (!(domainIbGibs && domainIbGibs.length > 0) &&
                !(tjpAddrs && tjpAddrs.length > 0)) {
                throw new Error(`(UNEXPECTED) domainIbGibs and tjpAddrs falsy/empty? we need one or the other (E: f674285111c8648398cd79d8c08ec826)`);
            }
            if ((domainIbGibs && domainIbGibs.length > 0) &&
                (tjpAddrs && tjpAddrs.length > 0)) {
                throw new Error(`(UNEXPECTED) both domainIbGibs and tjpAddrs truthy? only pass in one or the other. (E: f674285111c8648398cd79d8c08ec826)`);
            }
            let tjps = [];
            if (tjpAddrs) {
                tjps = tjpAddrs;
            }
            else if (domainIbGibs && domainIbGibs.length > 0) {
                // Extract TJPs from domain Ibgibs
                // if (false) { console.log(`${lc} domainIbGibs (${domainIbGibs.length}) provided.  (I: a378995a0658af1f086ac1f297486c26)`); }
                const { mapWithTjp_YesDna, mapWithTjp_NoDna } = splitPerTjpAndOrDna({ ibGibs: domainIbGibs });
                if (false) {
                    console.log(`${lc}[TEST DEBUG] mapWithTjp_YesDna: ${JSON.stringify(mapWithTjp_YesDna)} (I: 287e22897148298e185712c8d50cfb26)`);
                }
                if (false) {
                    console.log(`${lc}[TEST DEBUG] mapWithTjp_NoDna: ${JSON.stringify(mapWithTjp_NoDna)} (I: 1bdc62656294aed0f9df334647dc7326)`);
                }
                const allWithTjp = [...Object.values(mapWithTjp_YesDna), ...Object.values(mapWithTjp_NoDna)];
                const timelineMap = getTimelinesGroupedByTjp({ ibGibs: allWithTjp });
                if (false) {
                    console.log(`${lc}[TEST DEBUG] timelineMap: ${JSON.stringify(timelineMap)} (I: 2cc04898e5f85179fb1ac7f827abc426)`);
                }
                tjps = Object.keys(timelineMap);
                if (false) {
                    console.log(`${lc}[TEST DEBUG] tjps: ${tjps} (I: 3dd548667cbd967c68e57c88dc570826)`);
                }
            }
            else {
                // No info provided. Return empty? Or throw?
                // User test context implied "everything", but implementation requires scope.
                console.warn(`${lc} No domainIbGibs or tjpAddrs provided. Returning empty KV.`);
                return {};
            }
            if (tjps.length === 0) {
                return {};
            }
            if (false) {
                console.log(`${lc} getting latest addrs for tjps: ${tjps} (I: d4e7080b8ba8187c583b82fd91ac0626)`);
            }
            const res = await getLatestAddrs({ space, tjpAddrs: tjps });
            if (!res.data || !res.data.latestAddrsMap) {
                throw new Error(`${lc} Failed to get latest addrs. (E: 7a8b9c0d)`);
            }
            // if (false) { console.log(`${lc}[TEST DEBUG] res.data.latestAddrsMap: ${JSON.stringify(res.data.latestAddrsMap)} (I: a8e128bdf80898ac2e6d8021a5bff726)`); }
            return res.data.latestAddrsMap;
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    async analyzeTimelines({ domainIbGibs, space, }) {
        const lc = `${this.lc}[${this.analyzeTimelines.name}]`;
        const srcGraph = await getDependencyGraph({
            ibGibs: domainIbGibs,
            live: true,
            space,
        });
        const srcGraphIsValid = srcGraph && Object.keys(srcGraph).length > 0;
        if (logalot) {
            console.log(`${lc} graph generated. nodes: ${srcGraphIsValid ? Object.keys(srcGraph).length : 0}`);
        }
        const srcIbGibs = srcGraphIsValid ? Object.values(srcGraph) : [];
        const { mapWithTjp_YesDna: srcMapWithTjp_YesDna, mapWithTjp_NoDna: srcMapWithTjp_NoDna, mapWithoutTjps: src_MapWithoutTjps } = splitPerTjpAndOrDna({ ibGibs: srcIbGibs });
        const srcStones = Object.values(src_MapWithoutTjps);
        const srcLiving = [...Object.values(srcMapWithTjp_YesDna), ...Object.values(srcMapWithTjp_NoDna)];
        const srcTimelinesMap = getTimelinesGroupedByTjp({ ibGibs: srcLiving });
        const srcSortedTjps = this.sortTimelinesTopologically(srcTimelinesMap);
        return { srcStones, srcTimelinesMap, srcSortedTjps, srcGraph };
    }
    /**
     * Creates the Initial Saga Frame (Init Stage).
     *
     * @remarks
     * **Execution Context**: **Sender (Local)**.
     *
     * Generates the first frame containing the Knowledge Vector of the Local Space.
     * This is sent to the Receiver to begin Gap Analysis.
     */
    async createInitFrame({ sagaId, sessionIdentity, localSpace, domainIbGibs, tempSpace, metaspace, conflictStrategy, }) {
        const lc = `${this.lc}[${this.createInitFrame.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 551af8b411ae9be712ce3358d43ee726)`);
            }
            // Analyze Timelines & Stones
            const analysis = await this.analyzeTimelines({ domainIbGibs, space: localSpace });
            // this is a lot, so uncomment this only if you want even more logging specific to this analysis
            // if (logalot) { console.log(`${lc} analysis: ${pretty(analysis)}(I: cd00e2be5eccc8976879c888ff2dfb26)`); } 
            const { srcTimelinesMap, srcGraph, srcStones, } = analysis;
            const initData = {
                sagaId,
                stage: SyncStage.init,
                knowledgeVector: {},
                identity: sessionIdentity, // KeystoneIbGib is already public data
                mode: SyncMode.sync,
                stones: srcStones.map(s => getIbGibAddr({ ibGib: s })),
            };
            // Populate Knowledge Vector
            Object.keys(srcTimelinesMap).forEach(tjp => {
                const timeline = srcTimelinesMap[tjp];
                const tip = timeline.at(-1);
                initData.knowledgeVector[tjp] = getIbGibAddr({ ibGib: tip });
            });
            if (logalot) {
                console.log(`${lc} SyncStage.init: ${SyncStage.init}, SyncStage.commit: ${SyncStage.commit}`);
                console.log(`${lc} initData.stage: ${initData.stage}`);
            }
            const initStone = await this.createSyncMsgStone({
                data: initData,
                space: tempSpace,
                metaspace
            });
            // if (logalot) { console.log(`${lc} initStone: ${pretty(initStone)} (I: 06e532f8a408549069474e96bed44826)`); }
            const sagaFrame = await this.evolveSyncSagaIbGib({
                msgStones: [initStone],
                identity: sessionIdentity,
                space: tempSpace,
                metaspace,
                conflictStrategy,
            });
            // IMMEDIATELY persist to both spaces for audit trail
            await this.ensureSagaFrameInBothSpaces({
                frame: sagaFrame,
                destSpace: localSpace, // localSpace is the Sender's destSpace
                tempSpace,
                metaspace
            });
            // if (logalot) { console.log(`${lc} sagaFrame (init): ${pretty(sagaFrame)} (I: b3d6a8be69248f18713cc3073cb08626)`); }
            return { sagaFrame, srcGraph };
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    /**
     * Helper to poll for streaming domain payloads and put them in the
     * local {@link tempSpace}.
     */
    async pollForDomainPayloads({ expectedAddrs, domainPayloadsMap, tempSpace, }) {
        const lc = `${this.lc}[${this.pollForDomainPayloads.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 26dce86bfca572939885798802d6e926)`);
            }
            let pending = [...expectedAddrs];
            const start = Date.now();
            /**
             * This needs
             */
            const timeoutMs = 5 * 60 * 1000; // 5 minutes...arbitrary at this point. This needs to be pulled out and improved eesh.
            while (pending.length > 0) {
                if (Date.now() - start > timeoutMs) {
                    throw new Error(`Timeout waiting for payloads: ${pending.join(', ')} (E: 46e1683c9578095261aaf798bd5e1826)`);
                }
                const stillPending = [];
                const found = [];
                for (const addr of pending) {
                    if (domainPayloadsMap.has(addr)) {
                        found.push(domainPayloadsMap.get(addr));
                        domainPayloadsMap.delete(addr);
                    }
                    else {
                        stillPending.push(addr);
                    }
                }
                if (found.length > 0) {
                    await putInSpace({ space: tempSpace, ibGibs: found });
                }
                pending = stillPending;
                if (pending.length > 0) {
                    await new Promise(resolve => setTimeout(resolve, 50));
                }
            }
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    async handleSagaFrame({ sagaIbGib, srcGraph, destSpace, tempSpace, identity, identitySecret, metaspace, }) {
        const lc = `${this.lc}[${this.handleSagaFrame.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 5deec8a1f7a6d263c88cd458ad990826)`);
            }
            if (!sagaIbGib.data) {
                throw new Error(`(UNEXPECTED) sagaIbGib.data falsy? (E: 71b938adf1d87c2527bfd4f86dfd0826)`);
            }
            if (logalot) {
                console.log(`${lc} sagaIbGib: ${pretty(sagaIbGib)} (I: 1b99d87d262e9d18d8a607a80b1a0126)`);
            }
            // Get Stage from Stone (or Frame for Init fallback)
            const { stage, messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
            if (logalot) {
                console.log(`${lc} handling frame stage: ${stage}`);
            }
            switch (stage) {
                case SyncStage.init:
                    return await this.handleInitFrame({ sagaIbGib, messageData, metaspace, destSpace, tempSpace, identity, identitySecret });
                case SyncStage.ack:
                    return await this.handleAckFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity });
                case SyncStage.delta:
                    return await this.handleDeltaFrame({ sagaIbGib, srcGraph, metaspace, destSpace, tempSpace, identity, });
                case SyncStage.commit:
                    return await this.handleCommitFrame({ sagaIbGib, metaspace, destSpace, tempSpace, identity, });
                default:
                    throw new Error(`${lc} (UNEXPECTED) Unknown sync stage: ${stage} (E: 9c2b4c8a6d34469f8263544710183355)`);
            }
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    // #region Handlers
    /**
     * Handles the `Init` frame.
     *
     * @remarks
     * **Execution Context**: **Receiver (Remote)**.
     *
     * The Receiver performs Gap Analysis here:
     * 1.  Compares Sender's Knowledge Vector (in `sagaIbGib`) vs Receiver's Local KV.
     * 2.  Identifies what Sender needs (`pushOfferAddrs`).
     * 3.  Identifies what Receiver needs (`deltaReqAddrs`).
     * 4.  Returns an `Ack` frame containing these lists.
     */
    async handleInitFrame({ sagaIbGib, messageData, destSpace, tempSpace, metaspace, identity, identitySecret, }) {
        const lc = `${this.lc}[${this.handleInitFrame.name}]`;
        console.log(`${lc} [TEST DEBUG] Received destSpace: ${destSpace.data?.name || destSpace.ib} (uuid: ${destSpace.data?.uuid || '[no uuid]'})`);
        if (logalot) {
            console.log(`${lc} starting...`);
        }
        // Extract Init Data
        const initData = messageData; // Using renamed variable for clarity
        if (initData.stage !== SyncStage.init) {
            throw new Error(`${lc} Invalid init frame: initData.stage !== SyncStage.init (E: 8a2b3c4d5e6f7g8h)`);
        }
        // if (logalot) { console.log(`${lc} initData: ${pretty(initData)} (I: 46b0f8441b96ad7a388f1ce3239dd826)`); }
        if (!initData || !initData.knowledgeVector) {
            throw new Error(`${lc} Invalid init frame: missing knowledgeVector (E: ed02c869e028d2d06841b9c7f80f2826)`);
        }
        // Determine Strategy from Saga Data (since V1 stores it in root)
        const conflictStrategy = sagaIbGib.data.conflictStrategy || 'abort';
        // 2. Gap Analysis
        const conflicts = [];
        const deltaReqAddrs = [];
        const pushOfferAddrs = [];
        // Stones Analysis (Constants / Non-TJPs)
        const stones = initData.stones || [];
        if (stones.length > 0) {
            if (logalot) {
                console.log(`${lc} processing stones: ${stones.length}`);
            }
            // Check if we have these stones
            const resStones = await getFromSpace({ space: destSpace, addrs: stones });
            const addrsNotFound = resStones.rawResultIbGib?.data?.addrsNotFound;
            if (addrsNotFound && addrsNotFound.length > 0) {
                if (logalot) {
                    console.log(`${lc} stones missing (requesting): ${addrsNotFound.length}`);
                }
                addrsNotFound.forEach(addr => {
                    if (!deltaReqAddrs.includes(addr)) {
                        deltaReqAddrs.push(addr);
                    }
                });
            }
        }
        const remoteKV = initData.knowledgeVector;
        if (logalot) {
            console.log(`${lc} remoteKV: ${pretty(remoteKV)} (I: 9f957862356dfeae183c200854e86e26)`);
        }
        const remoteTjps = Object.keys(remoteKV);
        console.log(`${lc} [TEST DEBUG] remoteTjps: ${JSON.stringify(remoteTjps)}`);
        if (logalot) {
            console.log(`${lc} remoteTjps: ${pretty(remoteTjps)} (I: 86ea4c53db0dc184c8b253386c402126)`);
        }
        // 1. Get Local Latest Addrs for all TJPs
        let localKV = {};
        if (remoteTjps.length > 0) {
            // Batch get latest addrs for the TJPs
            const resGetLatestAddrs = await getLatestAddrs({
                space: destSpace,
                tjpAddrs: remoteTjps,
            });
            if (!resGetLatestAddrs.data) {
                throw new Error(`(UNEXPECTED) resGetLatestAddrs.data falsy? (E: b180d813c088042b38e1e02e06a16926)`);
            }
            if (!resGetLatestAddrs.data.latestAddrsMap) {
                throw new Error(`(UNEXPECTED) resGetLatestAddrs.data.latestAddrsMap falsy? (E: 16bc386dd51d0ff53a49620b1e641826)`);
            }
            localKV = resGetLatestAddrs.data.latestAddrsMap;
            console.log(`${lc} [TEST DEBUG] localKV: ${JSON.stringify(localKV)}`);
            if (logalot) {
                console.log(`${lc} localKV: ${pretty(localKV)} (I: 980975642cbccd8018cf0cd808d30826)`);
            }
        }
        // 2. Gap Analysis
        for (const tjp of remoteTjps) {
            const remoteAddr = remoteKV[tjp];
            const localAddr = localKV[tjp];
            if (!localAddr) {
                // We (Receiver) don't have this timeline. Request it.
                console.log(`${lc} [TEST DEBUG] Missing local timeline for TJP: ${tjp}. Requesting remoteAddr: ${remoteAddr}`);
                deltaReqAddrs.push(remoteAddr);
                continue;
            }
            if (localAddr === remoteAddr) {
                // Synced
                console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Synced (localAddr === remoteAddr)`);
                continue;
            }
            console.log(`${lc} [TEST DEBUG] TJP ${tjp}: localAddr=${localAddr}, remoteAddr=${remoteAddr} - checking for divergence...`);
            // Check if Remote is in Local's PAST (Local is Ahead -> Push Offer)
            // (Sender has older version, Receiver has newer) -> Receiver Offers Push
            const isRemoteInPast = await isPastFrame({
                olderAddr: remoteAddr,
                newerAddr: localAddr,
                space: destSpace,
            });
            if (isRemoteInPast) {
                console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Remote is in past - offering push`);
                pushOfferAddrs.push(localAddr);
            }
            else {
                // Remote is not in our past. 
                // Either Remote is ahead (Fast-Backward) OR Diverged.
                // Check if Local is in Remote's PAST (Remote is Ahead -> Delta Request)
                const isLocalInPast = await isPastFrame({
                    olderAddr: localAddr,
                    newerAddr: remoteAddr,
                    space: destSpace,
                });
                if (isLocalInPast) {
                    // Fast-Forward: We update to remote's tip.
                    console.log(`${lc} [TEST DEBUG] TJP ${tjp}: Local is in past - requesting delta`);
                    deltaReqAddrs.push(remoteAddr);
                }
                else {
                    // DIVERGENCE: Both have changes the other doesn't know about.
                    console.log(`${lc} [TEST DEBUG] TJP ${tjp}: DIVERGENCE DETECTED! conflictStrategy=${conflictStrategy}`);
                    if (conflictStrategy === 'abort') {
                        // Abort Strategy: We will treat this as terminal.
                        // But for Unified Ack, we just mark it terminal in the list?
                        // Or do we actually throw/abort the saga?
                        // Current logic (below) aborts the saga if ANY conflict is terminal/abort.
                        conflicts.push({
                            tjpAddr: tjp,
                            localAddr: localAddr,
                            remoteAddr,
                            timelineAddrs: [], // Not needed for abort
                            reason: 'divergence',
                            terminal: true
                        });
                    }
                    else if (conflictStrategy === 'optimistic') {
                        // Optimistic: We want to resolving this.
                        // We need to send our history to the Sender so they can Merge.
                        // Fetch Full History for Local Timeline
                        // Note: We might optimize this to only send "recent" history if we had a KV?
                        // But for now, get full past.
                        // Optimization: localKV might not have full history.
                        // We need to inspect the 'past' of the local tip.
                        // We need the ACTUAL object to get the past.
                        // We have localAddr.
                        const resLocalTip = await getFromSpace({ space: destSpace, addr: localAddr });
                        const localTip = resLocalTip.ibGibs?.[0];
                        if (!localTip) {
                            throw new Error(`${lc} Failed to load local tip for conflict resolution. (E: 8f9b2c3d4e5f6g7h)`);
                        }
                        const timelineAddrs = [localAddr, ...(localTip.rel8ns?.past || [])];
                        conflicts.push({
                            tjpAddr: tjp,
                            localAddr: localAddr,
                            remoteAddr,
                            timelineAddrs,
                            reason: 'divergence',
                            terminal: false
                        });
                    }
                    else {
                        throw new Error(`${lc} Unsupported conflict strategy: ${conflictStrategy} (E: 2a9b3c4d5e6f7g8h9i0j)`);
                    }
                }
            }
        }
        // Check if we should ABORT (if any conflict is terminal)
        const hasTerminalConflicts = conflicts.some(c => c.terminal);
        if (hasTerminalConflicts) {
            // Abort Strategy: Kill the saga.
            if (logalot) {
                console.warn(`${lc} ABORTING Sync Saga due to terminal conflicts: ${JSON.stringify(conflicts)}`);
            }
            // We reuse the ConflictData structure for terminal aborts?
            // Or do we send an Ack with terminal conflicts?
            // Original design had explicit Conflict Frame for Abort.
            // Let's stick to that for purely terminal cases to be safe/explicit?
            // Or Unified: Just send Ack with terminal=true conflicts. Sender sees them and aborts.
            // Decision: Unified Ack for everything is cleaner protocol.
            // But wait, the original code below creates a Conflict Stone.
            // Let's preserve the explicit 'Conflict' frame for total aborts if that's easier,
            // OR fully switch to Ack.
            // Protocol states: Init -> Ack. If Ack contains terminal errors, Sender can Commit(Fail).
            // Let's use Ack with conflicts.
        }
        // 2. Add Push Offers (Missing in Local)
        // Check if we have them. If not, ask for them.
        for (const addr of pushOfferAddrs) {
            const hasIt = await getFromSpace({ addr, space: destSpace });
            if (!hasIt.success || !hasIt.ibGibs || hasIt.ibGibs.length === 0) {
                // If we don't have it, we put it in `deltaReqAddrs` of the Ack.
                deltaReqAddrs.push(addr);
            }
        }
        // 3. Build Knowledge Vector (Full History for known timelines)
        // [NEW] Smart Diff
        // We iterate over all relevant addresses (deltas we are requesting OR push offers we might have newer versions of).
        // Since we are "reacting" to Init, we primarily want to tell the Sender what we DO have for the things they talked about.
        const knowledgeVector = {};
        const relevantAddrs = new Set([...pushOfferAddrs, ...deltaReqAddrs]);
        // [Smart Diff] Populate knowledge from timelines identified by Sender
        for (const tjp of remoteTjps) {
            const localAddr = localKV[tjp];
            if (localAddr) {
                const res = await getFromSpace({ addr: localAddr, space: destSpace });
                if (res.success && res.ibGibs?.[0]) {
                    const ibGib = res.ibGibs[0];
                    const realTjp = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib }); // Should match `tjp` if normalized 
                    if (!knowledgeVector[realTjp]) {
                        const past = ibGib.rel8ns?.past || [];
                        knowledgeVector[realTjp] = [getIbGibAddr({ ibGib }), ...past];
                    }
                }
            }
        }
        // [Smart Diff] Also check individual requested addresses (Fall back for constants/unknown TJPs)
        for (const addr of relevantAddrs) {
            // Only process if not already covered by TJP logic above
            // We can't really know if it's covered easily without resolving. 
            // But if we don't have it (requesting), we won't find it here anyway.
            // If we DO have it (push offer), we might find it.
            const res = await getFromSpace({ addr, space: destSpace });
            if (res.success && res.ibGibs?.[0]) {
                const ibGib = res.ibGibs[0];
                const tjpAddr = ibGib.rel8ns?.tjp?.[0] || getIbGibAddr({ ibGib });
                if (!knowledgeVector[tjpAddr]) {
                    const past = ibGib.rel8ns?.past || [];
                    knowledgeVector[tjpAddr] = [getIbGibAddr({ ibGib }), ...past];
                }
            }
            else {
                // We don't have `addr`.
            }
        }
        // Also populate from `knowledgeVector` in Init if we want bidirectional? 
        // No, `Init` doesn't have `knowledgeVector` in `SyncSagaMessageInitData` yet (it has `SyncInitData` generic props).
        // Let's assume standard flow:
        // 1. Sender says "I have X"
        // 2. Receiver says "I don't have X. But if I did have Y (ancestor), I'd tell you." 
        // Problem: Receiver doesn't know X is related to Y without X.
        // SOLUTION:
        // The *Sender* must include TJP mappings or we rely on `knowledgeVector` in `Init`?
        // `SyncSagaMessageInitData_V1` extends `SyncInitData`.
        // `SyncInitData` has `knowledgeVector`.
        // If Sender populates `knowledgeVector` in `Init`, Receiver can use keys (TJPs) to look up its own state!
        // Let's implement Sender populating `Init.knowledgeVector`.
        // But `SyncSagaCoordinator.startSaga` creates Init.
        // 3. Create Ack Frame
        const sagaId = sagaIbGib.data.uuid;
        // Create Payload Stone
        const ackData = {
            sagaId,
            stage: SyncStage.ack,
            deltaReqAddrs,
            pushOfferAddrs,
            knowledgeVector,
            conflicts: conflicts.length > 0 ? conflicts : undefined, // Include conflicts if any detected
        };
        const ackStone = await this.createSyncMsgStone({
            data: ackData,
            space: tempSpace,
            metaspace,
        });
        if (logalot) {
            console.log(`${lc} ackStone created: ${pretty(ackStone)} (I: 313708132dd53ff946befb7833657826)`);
        }
        // Evolve Saga
        const ackFrame = await this.evolveSyncSagaIbGib({
            prevSagaIbGib: sagaIbGib,
            msgStones: [ackStone],
            identity,
            space: tempSpace,
            metaspace,
        });
        // IMMEDIATELY persist to both spaces for audit trail (before any errors can occur)
        await this.ensureSagaFrameInBothSpaces({ frame: ackFrame, destSpace, tempSpace, metaspace });
        // if (logalot) { console.log(`${lc} ackFrame created: ${pretty(ackFrame)} (I: be24480592eec478086bb3da49286826)`); }
        // Build control payloads: frame + its dependencies (msg stone, identity)
        const payloadIbGibsControl = [ackFrame, ackStone];
        if (identity) {
            payloadIbGibsControl.push(identity);
        }
        return { frame: ackFrame, payloadIbGibsControl };
    }
    /**
     * Handles the `Ack` frame.
     *
     * @remarks
     * **Execution Context**: **Sender (Local)**.
     *
     * The Sender reacts to the Receiver's requirements:
     * 1.  `deltaReqAddrs`: Receiver wants this data. Sender gathers it from `srcGraph` and puts it in `payloadIbGibs` (for next frame).
     * 2.  `pushOfferAddrs`: Receiver has newer data. Sender acknowledges and adds them to `requests` (asking Receiver to send them).
     *
     * Returns a `Delta` frame.
     */
    async handleAckFrame({ sagaIbGib, srcGraph, destSpace, tempSpace, metaspace, identity, }) {
        const lc = `${this.lc}[${this.handleAckFrame.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 605b6860e898267a5b50c6d85704be26)`);
            }
            const { messageData, } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
            const ackData = messageData;
            if (!ackData) {
                throw new Error(`${lc} ackData falsy (E: 3b8415edc876084c88a25b98e2d55826)`);
            }
            if (ackData.stage !== SyncStage.ack) {
                throw new Error(`${lc} Invalid ack frame: ackData.stage !== SyncStage.ack (E: 2e8b0a94b5954a66a6a1a7a0b3f5b7a1)`);
            }
            if (logalot) {
                console.log(`${lc} ackData: ${pretty(ackData)} (I: 7f8e9d0a1b2c3d4e5f6g7h8i9j0k)`);
            }
            // 1. Check for Conflicts
            const conflicts = ackData.conflicts || [];
            console.log(`${lc} [CONFLICT DEBUG] Received conflicts from Ack: ${conflicts.length}`);
            if (conflicts.length > 0) {
                console.log(`${lc} [CONFLICT DEBUG] Conflicts detail: ${JSON.stringify(conflicts, null, 2)}`);
            }
            const terminalConflicts = conflicts.filter(c => c.terminal);
            if (terminalConflicts.length > 0) {
                console.warn(`${lc} Received terminal conflicts from Ack: ${JSON.stringify(terminalConflicts)}`);
                // Terminal failure. Sender should probably Commit(Fail) or just Abort.
                // For now, throw to trigger abort.
                throw new Error(`${lc} Peer reported terminal conflicts. (E: a1b2c3d4e5f6g7h8i9j0k)`);
            }
            const optimisticConflicts = conflicts.filter(c => !c.terminal);
            const mergeDeltaReqs = []; // Additional requests for merging
            if (optimisticConflicts.length > 0) {
                console.log(`${lc} [CONFLICT DEBUG] Processing ${optimisticConflicts.length} optimistic conflicts`);
                // We need to resolve these.
                // Strategy:
                // 1. Analyze Divergence (Sender vs Receiver)
                // 2. Identify missing data needed for merge (Receiver's unique frames)
                // 3. Request that data (as Delta Reqs)
                // 4. (Later in Delta Phase) Perform Merge.
                // BUT: The Delta Phase is usually generic "Send me these Addrs".
                // If we just add to `deltaReqAddrs` (which are requests for Sender to send to Receiver?),
                // wait. `ackData.deltaReqAddrs` are what RECEIVER wants from SENDER.
                // We (Sender) are processing the Ack.
                // We need to request data FROM Receiver.
                // But the protocol 'Ack' step typically leads to 'Delta' (Sender sending data).
                // If Sender needs data from Receiver, it usually happens in 'Pull' mode or a separate request?
                // Or can we send a 'Delta Request' frame?
                // Standard Saga: Init(Push) -> Ack(Pull Reqs) -> Delta(Push Data).
                // If Sender needs data, we might need a "Reverse Delta" or "Pull" phase?
                // Or we just proceed to Delta (sending what Receiver wants), 
                // AND we piggyback our own requests?
                // OR: We treat the Conflict Resolution as a sub-saga or side-effect?
                // SIMPLIFICATION for V1:
                // If we need data to merge, we must get it.
                // We are the Coordinator (Active). We can fetch from Peer immediately?
                // `peer.pull(addr)`?
                // Yes! The Coordinator has the `peer`.
                // Let's analyze and pull immediately.
                for (const conflict of optimisticConflicts) {
                    const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
                    // Sender History
                    // We need our own history for this timeline.
                    // We know the 'senderTip' (remoteAddr in Ack).
                    // Sender should verify it has this tip.
                    // Compute Diffs
                    // We need to find `receiverOnly` addrs.
                    // Receiver sent us `timelineAddrs` (Full History).
                    const receiverHistorySet = new Set(timelineAddrs);
                    // We need our execution context's history for this senderTip.
                    // We can fetch valid 'past' from space.
                    const resSenderTip = await getFromSpace({ space: destSpace, addr: senderTip });
                    const senderTipIbGib = resSenderTip.ibGibs?.[0];
                    if (!senderTipIbGib) {
                        throw new Error(`${lc} Sender missing its own tip? ${senderTip} (E: 9c8d7e6f5g4h3i2j1k0l)`);
                    }
                    // Basic Diff: Find what Receiver has that we don't.
                    // Actually, we need to traverse OUR past to find commonality.
                    const senderHistory = [senderTip, ...(senderTipIbGib.rel8ns?.past || [])];
                    const receiverOnlyAddrs = timelineAddrs.filter(addr => !senderHistory.includes(addr));
                    if (receiverOnlyAddrs.length > 0) {
                        console.log(`${lc} [CONFLICT DEBUG] Found ${receiverOnlyAddrs.length} receiver-only frames - need to pull for merge`);
                        console.log(`${lc} [CONFLICT DEBUG] Receiver-only addrs:`, receiverOnlyAddrs);
                        // PULL these frames from Peer into Local Space
                        // (Validation: We trust peer for now / verification happens on put)
                        for (const addr of receiverOnlyAddrs) {
                            console.error(`${lc} [CONFLICT DEBUG] NOT IMPLEMENTED (E: e6bf1a9d2758c469bb2f97514062d826)`);
                        }
                        // Compute DELTA dependencies for each receiver-only frame
                        // Find LCA to determine what dependencies we already have
                        const lcaAddr = timelineAddrs.find(addr => senderHistory.includes(addr));
                        console.log(`${lc} [CONFLICT DEBUG] LCA: ${lcaAddr || 'NONE'}`);
                        const skipAddrsSet = new Set();
                        if (lcaAddr) {
                            try {
                                const lcaRes = await getFromSpace({ addr: lcaAddr, space: destSpace });
                                const lcaIbGib = lcaRes.ibGibs?.[0];
                                if (lcaIbGib) {
                                    const lcaDeps = await getDependencyGraph({ ibGib: lcaIbGib, space: destSpace });
                                    if (lcaDeps)
                                        Object.keys(lcaDeps).forEach(a => skipAddrsSet.add(a));
                                    console.log(`${lc} [CONFLICT DEBUG] LCA deps to skip: ${skipAddrsSet.size}`);
                                }
                            }
                            catch (e) {
                                console.warn(`${lc} Error getting LCA deps: ${extractErrorMsg(e)}`);
                            }
                        }
                        // For each receiver-only frame, get its DELTA dependency graph (minus LCA deps)
                        for (const addr of receiverOnlyAddrs) {
                            // Add the frame itself first
                            if (!mergeDeltaReqs.includes(addr)) {
                                mergeDeltaReqs.push(addr);
                            }
                            // Get the frame's delta dependencies (skip LCA's deps)
                            try {
                                const frameRes = await getFromSpace({ addr, space: destSpace });
                                const frameIbGib = frameRes.ibGibs?.[0];
                                if (frameIbGib) {
                                    // Get dependency graph, skipping all LCA dependencies
                                    const frameDeltaDeps = await getDependencyGraph({
                                        ibGib: frameIbGib,
                                        space: destSpace,
                                        skipAddrs: Array.from(skipAddrsSet), // Skip entire LCA dep graph
                                    });
                                    if (frameDeltaDeps) {
                                        // Add all delta dependencies (Object.keys gives us the addresses)
                                        Object.keys(frameDeltaDeps).forEach(depAddr => {
                                            if (!mergeDeltaReqs.includes(depAddr) && !skipAddrsSet.has(depAddr)) {
                                                mergeDeltaReqs.push(depAddr);
                                            }
                                        });
                                    }
                                }
                            }
                            catch (depError) {
                                console.warn(`${lc} [CONFLICT DEBUG] Error getting delta deps for ${addr}: ${extractErrorMsg(depError)}`);
                            }
                        }
                        console.log(`${lc} [CONFLICT DEBUG] Total merge requests (frames + delta deps): ${mergeDeltaReqs.length}`);
                    }
                    else {
                        console.log(`${lc} [CONFLICT DEBUG] No receiver-only frames found for this conflict`);
                    }
                }
                console.log(`${lc} [CONFLICT DEBUG] Finished processing ${optimisticConflicts.length} conflicts. mergeDeltaReqs: ${mergeDeltaReqs.length}`);
            }
            else {
                console.log(`${lc} [CONFLICT DEBUG] No optimistic conflicts to process`);
            }
            // 2. Prepare Delta Payload (What Receiver Requesting + Our Conflict Logic)
            const deltaReqAddrs = ackData.deltaReqAddrs || [];
            const pushOfferAddrs = ackData.pushOfferAddrs || [];
            // 1. Process Push Offers (Pull Requests) (Naive: Accept all if missing)
            const pullReqAddrs = [];
            for (const addr of pushOfferAddrs) {
                const existing = srcGraph[addr] || (await getFromSpace({ addr, space: destSpace })).ibGibs?.[0];
                if (!existing) {
                    pullReqAddrs.push(addr);
                }
            }
            // 2. Process Delta Requests (Push Payload)
            // [NEW] Smart Diff: Use knowledgeVector to skip dependencies
            const skipAddrs = new Set();
            if (ackData.knowledgeVector) {
                Object.values(ackData.knowledgeVector).forEach(addrs => {
                    addrs.forEach(a => skipAddrs.add(a));
                });
            }
            const payloadIbGibs = [];
            // Gather all tips to sync first
            const tipsToSync = [];
            for (const addr of deltaReqAddrs) {
                let ibGib = srcGraph[addr];
                if (!ibGib) {
                    const res = await getFromSpace({ addr, space: destSpace });
                    if (res.ibGibs && res.ibGibs.length > 0) {
                        ibGib = res.ibGibs[0];
                    }
                }
                if (ibGib) {
                    tipsToSync.push(ibGib);
                }
                else {
                    throw new Error(`${lc} Requested addr not found: ${addr} (E: d41d59cff4a887f6414c3e92eabd8e26)`);
                }
            }
            // Calculate Dependency Graph for ALL tips, effectively utilizing common history
            // Pass skipAddrs to `getDependencyGraph` or gather manually.
            // `getDependencyGraph` takes a single ibGib.
            // We can optimize by doing it for each tip and unioning the result? 
            // Or `graph-helper` could support `ibGibs: []`. It currently takes `ibGib`.
            // We will loop.
            const allDepsSet = new Set();
            for (const tip of tipsToSync) {
                // Always include the tip itself
                const tipAddr = getIbGibAddr({ ibGib: tip });
                // Only process if not skipped (though deltaReq implies they barely just asked for it)
                // But detailed deps might be skipped.
                // Get Graph with Skips
                // Logic: "Give me everything related to Tip, EXCEPT X, Y, Z"
                const deps = await getDependencyGraph({
                    ibGib: tip,
                    space: destSpace,
                    skipAddrs: Array.from(skipAddrs)
                });
                // [FIX] Ensure Tip is included if not in deps (e.g. constant with no rel8ns)
                let tipIncluded = false;
                if (deps) {
                    Object.values(deps).forEach(d => {
                        const dAddr = getIbGibAddr({ ibGib: d });
                        if (!allDepsSet.has(dAddr)) {
                            allDepsSet.add(dAddr);
                            payloadIbGibs.push(d);
                        }
                        if (dAddr === tipAddr) {
                            tipIncluded = true;
                        }
                    });
                }
                if (!tipIncluded && !skipAddrs.has(tipAddr)) {
                    if (logalot) {
                        console.log(`${lc} Tip not in deps, adding explicitly: ${tipAddr}`);
                    }
                    if (!allDepsSet.has(tipAddr)) {
                        allDepsSet.add(tipAddr);
                        payloadIbGibs.push(tip);
                    }
                }
            }
            // 3. Create Delta Frame
            const sagaId = ackData.sagaId;
            const deltaData = {
                sagaId: sagaIbGib.data.uuid,
                stage: SyncStage.delta,
                payloadAddrs: payloadIbGibs.map(p => getIbGibAddr({ ibGib: p })),
                requests: [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])].length > 0 ? [...(pullReqAddrs || []), ...(mergeDeltaReqs || [])] : undefined,
            };
            if (logalot) {
                console.log(`${lc} Creating Delta Stone. Data stage: ${deltaData.stage}`);
            }
            const deltaStone = await this.createSyncMsgStone({
                data: deltaData,
                space: tempSpace,
                metaspace,
            });
            const deltaFrame = await this.evolveSyncSagaIbGib({
                prevSagaIbGib: sagaIbGib,
                msgStones: [deltaStone],
                identity,
                space: tempSpace,
                metaspace,
            });
            // IMMEDIATELY persist to both spaces for audit trail
            await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
            if (logalot) {
                console.log(`${lc} Delta Frame created. Rel8ns: ${JSON.stringify(deltaFrame.rel8ns)}`);
            }
            // Build control payloads: frame + its dependencies (msg stone, identity)
            const payloadIbGibsControl = [deltaFrame, deltaStone];
            if (identity) {
                payloadIbGibsControl.push(identity);
            }
            return { frame: deltaFrame, payloadIbGibsControl, payloadIbGibsDomain: payloadIbGibs };
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    /**
     * Handles the `Delta` frame.
     *
     * @remarks
     * **Execution Context**: **Both**.
     *
     * 1.  **Ingestion**: Receives data sent by Peer (`payloadAddrs`). Resolves them from `Rel8ns.payload` (implicitly via space/context) and returns them in `receivedPayload` to be saved.
     * 2.  **Fulfillment**: Checks `requests`. If Peer requested data, gathers it and prepares `outgoingPayload`.
     * 3.  **Completion**: If no more requests, transitions to `Commit`.
     */
    async handleDeltaFrame({ sagaIbGib, srcGraph, destSpace, tempSpace, metaspace, identity, }) {
        const lc = `${this.lc}[${this.handleDeltaFrame.name}]`;
        if (logalot) {
            console.log(`${lc} starting...`);
        }
        const { messageData } = await this.getStageAndPayloadFromFrame({ ibGib: sagaIbGib, space: tempSpace });
        const deltaData = messageData;
        if (!deltaData) {
            throw new Error(`${lc} deltaData falsy (E: 7c28c8d8f08a4421b8344e6727271421)`);
        }
        if (deltaData.stage !== SyncStage.delta) {
            throw new Error(`${lc} Invalid delta frame: deltaData.stage !== SyncStage.delta (E: 0c28c8d8f08a4421b8344e6727271421)`);
        }
        if (logalot) {
            console.log(`${lc} deltaData: ${pretty(deltaData)} (I: 8d7e6f5g4h3i2j1k0l9m)`);
        }
        console.log(`${lc} [CONFLICT DEBUG] deltaData.payloadAddrs count: ${deltaData.payloadAddrs?.length || 0}`);
        const payloadAddrs = deltaData.payloadAddrs || [];
        const peerRequests = deltaData.requests || [];
        const peerProposesCommit = deltaData.proposeCommit || false;
        // 1. Process Received Payload (Ingest)
        const receivedPayloadIbGibs = [];
        if (payloadAddrs.length > 0) {
            // We use `payloadAddrs` as the manifest. 
            // The ACTUAL collection of ibGibs should be available via `getFromSpace` 
            // assuming the "Transport" layer put them there implicitly?
            // OR, if we are local-only, we just get them.
            // The `handleDeltaFrame` contract assumes data is reachable in `space`.
            const res = await getFromSpace({
                addrs: payloadAddrs,
                space: tempSpace, // Incoming data is in tempSpace
            });
            if (res.ibGibs) {
                receivedPayloadIbGibs.push(...res.ibGibs);
                // Also put them? `getFromSpace` retrieves. If they are in space, they are persisted.
                // If this is a Temp Space, they are safe.
            }
            else {
                console.warn(`${lc} Failed to retrieve payloads listed in delta: ${payloadAddrs.join(', ')}`);
            }
        }
        // 2. Fulfill Peer Requests (Outgoing Payload with Delta Dependencies)
        const outgoingPayload = [];
        const outgoingAddrsSet = new Set(); // Track what we've added
        console.log(`${lc} [CONFLICT DEBUG] Fulfilling ${peerRequests.length} peer requests`);
        for (const addr of peerRequests) {
            // Get the requested ibGib
            let ibGib = srcGraph[addr];
            if (!ibGib) {
                const res = await getFromSpace({ addr, space: destSpace }); // Query from destSpace  
                if (res.ibGibs && res.ibGibs.length > 0) {
                    ibGib = res.ibGibs[0];
                }
            }
            if (ibGib) {
                // Add the requested ibGib itself
                const ibGibAddr = getIbGibAddr({ ibGib });
                if (!outgoingAddrsSet.has(ibGibAddr)) {
                    outgoingPayload.push(ibGib);
                    outgoingAddrsSet.add(ibGibAddr);
                }
                // Expand to include full dependency graph for this ibGib
                // (Receiver needs all deps to properly process/merge)
                try {
                    const deps = await getDependencyGraph({
                        ibGib,
                        space: destSpace,
                    });
                    if (deps) {
                        Object.values(deps).forEach(depIbGib => {
                            const depAddr = getIbGibAddr({ ibGib: depIbGib });
                            if (!outgoingAddrsSet.has(depAddr)) {
                                outgoingPayload.push(depIbGib);
                                outgoingAddrsSet.add(depAddr);
                            }
                        });
                    }
                }
                catch (depError) {
                    console.warn(`${lc} [CONFLICT DEBUG] Error expanding deps for ${addr}: ${extractErrorMsg(depError)}`);
                }
            }
            else {
                console.warn(`${lc} Requested addr not found during delta fulfillment: ${addr}`);
            }
        }
        console.log(`${lc} [CONFLICT DEBUG] Outgoing payload size (with deps): ${outgoingPayload.length}`);
        // 3. Execute Merges (If applicable)
        // Check if we have pending conflicts that we CAN resolve now that we have data.
        // We look at the Saga History (Ack Frame) to find conflicts.
        // Optimization: Do this only if we received payloads.
        const mergeResultIbGibs = [];
        console.log(`${lc} [CONFLICT DEBUG] Checking for merge. receivedPayloadIbGibs.length: ${receivedPayloadIbGibs.length}`);
        if (receivedPayloadIbGibs.length > 0) {
            console.log(`${lc} [TEST DEBUG] Received Payloads (${receivedPayloadIbGibs.length}). Checking for conflicts/merges...`);
            // Find the Ack frame in history to get conflicts
            // Optimization: Batch fetch history from `sagaIbGib.rel8ns.past`
            // V1 timelines carry full history in `past`.
            const pastAddrs = sagaIbGib.rel8ns?.past || [];
            console.log(`${lc} [TEST DEBUG] pastAddrs count: ${pastAddrs.length}`);
            let ackData;
            if (pastAddrs.length > 0) {
                // Batch fetch all past frames
                const resPast = await getFromSpace({ addrs: pastAddrs, space: tempSpace });
                if (resPast.success && resPast.ibGibs) {
                    // Iterate backwards (most recent first) to find the latest Ack
                    for (let i = resPast.ibGibs.length - 1; i >= 0; i--) {
                        const pastFrame = resPast.ibGibs[i];
                        const messageStone = await getSyncSagaMessageFromFrame({
                            frameIbGib: pastFrame,
                            space: tempSpace
                        });
                        if (messageStone?.data?.stage === SyncStage.ack) {
                            ackData = messageStone.data;
                            console.log(`${lc} [TEST DEBUG] Found Ack Frame. Conflicts: ${ackData.conflicts?.length || 0}`);
                            break;
                        }
                    }
                }
            }
            if (ackData && ackData.conflicts) {
                const optimisticConflicts = ackData.conflicts.filter(c => !c.terminal);
                for (const conflict of optimisticConflicts) {
                    const { timelineAddrs, localAddr: receiverTip, remoteAddr: senderTip } = conflict;
                    // We are Sender (usually) here if we are merging.
                    // Check if we have the history needed (timelineAddrs).
                    // Specifically, we needed the `receiverOnly` parts.
                    // We blindly attempt merge if we have both tips accessible?
                    // We need `receiverTip` (localAddr in Ack) and `senderTip` (remoteAddr).
                    // Check if we have receiverTip in space
                    console.log(`${lc} [CONFLICT DEBUG] Attempting merge for conflict. ReceiverTip: ${receiverTip}, SenderTip: ${senderTip}`);
                    const resRecTip = await getFromSpace({ addr: receiverTip, space: tempSpace }); // Check tempSpace for incoming data
                    console.log(`${lc} [CONFLICT DEBUG] ReceiverTip found in tempSpace: ${!!resRecTip.ibGibs?.[0]}`);
                    if (resRecTip.success && resRecTip.ibGibs?.[0]) {
                        // We have the tip!
                        // Do we have the full history? 
                        // `mergeDivergentTimelines` in `conflict-optimistic` will attempt to fetch history.
                        // If we just ingested the missing pieces, `getFromSpace` inside `merge` should succeed.
                        // Perform Merge!
                        try {
                            const mergeResult = await mergeDivergentTimelines({
                                tipA: (await getFromSpace({ addr: senderTip, space: destSpace })).ibGibs[0], // Our tip from destSpace
                                tipB: resRecTip.ibGibs[0], // Their tip (from tempSpace)
                                space: tempSpace, // Merge uses tempSpace
                                metaspace,
                            });
                            if (mergeResult) {
                                console.log(`${lc} [TEST DEBUG] Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
                                if (logalot) {
                                    console.log(`${lc} Merge success! New Tip: ${getIbGibAddr({ ibGib: mergeResult })}`);
                                }
                                mergeResultIbGibs.push(mergeResult);
                                outgoingPayload.push(mergeResult); // Send result to peer
                            }
                        }
                        catch (e) {
                            console.error(`${lc} Merge failed: ${e}`);
                            // If merge fails, we might Abort or just continue?
                        }
                    }
                }
            }
        }
        // 4. Determine Next Action
        // We have `outgoingPayload` (Requests + Merge Results).
        // Does Peer have outstanding requests? No, we fulfilled `peerRequests`.
        // Do WE have outstanding requests?
        // We might if `mergeResult` requires further sync? Usually no, result is complete.
        const myRequests = []; // If we had more needs (e.g. partial payload), we'd add here.
        const hasOutgoing = outgoingPayload.length > 0;
        const hasMyRequests = myRequests.length > 0;
        if (hasOutgoing || hasMyRequests) {
            // We have business to attend to -> Send Delta
            const responseDeltaData = {
                sagaId: deltaData.sagaId,
                stage: SyncStage.delta,
                payloadAddrs: outgoingPayload.map(p => getIbGibAddr({ ibGib: p })),
                requests: hasMyRequests ? myRequests : undefined,
                proposeCommit: !hasMyRequests // If we are sending data but have no requests, we VALIDATE PROPOSAL?
                // Wait. If we send data, we are NOT committing yet. 
                // We are sending data. The OTHER side must ingest it.
                // So proposeCommit = true? 
                // "Here is the data. I'm done. If you are good, let's commit."
                // Yes.
            };
            // BUT if `peerProposesCommit` was true, and we are sending data, we are effectively rejecting/delaying it.
            // We just send the Delta. Peer receives it, ingests, sees ProposeCommit=True (from us), and then Commits.
            // So yes, proposeCommit = true.
            responseDeltaData.proposeCommit = true;
            const deltaStone = await this.createSyncMsgStone({
                data: responseDeltaData,
                space: tempSpace,
                metaspace
            });
            const deltaFrame = await this.evolveSyncSagaIbGib({
                prevSagaIbGib: sagaIbGib,
                msgStones: [deltaStone],
                identity,
                space: tempSpace,
                metaspace
            });
            // IMMEDIATELY persist to both spaces for audit trail
            await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
            // Build control payloads: frame + its dependencies (msg stone, identity)
            const payloadIbGibsControl = [deltaFrame, deltaStone];
            if (identity) {
                payloadIbGibsControl.push(identity);
            }
            return { frame: deltaFrame, payloadIbGibsControl, payloadIbGibsDomain: outgoingPayload };
        }
        else {
            // We have nothing to send.
            if (peerProposesCommit) {
                // Peer is done. We are done. -> Commit.
                const commitData = {
                    sagaId: deltaData.sagaId,
                    stage: SyncStage.commit,
                    success: true,
                };
                const commitStone = await this.createSyncMsgStone({
                    data: commitData,
                    space: tempSpace,
                    metaspace
                });
                const commitFrame = await this.evolveSyncSagaIbGib({
                    prevSagaIbGib: sagaIbGib,
                    msgStones: [commitStone],
                    identity,
                    space: tempSpace,
                    metaspace
                });
                // IMMEDIATELY persist to both spaces for audit trail
                await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
                // Build control payloads for commit
                const commitCtrlPayloads = [commitFrame, commitStone];
                if (identity) {
                    commitCtrlPayloads.push(identity);
                }
                return { frame: commitFrame, payloadIbGibsControl: commitCtrlPayloads };
            }
            else {
                // peer did NOT propose commit (maybe they just sent data/requests and didn't ready flag).
                // But we are empty.
                // So WE propose commit.
                const responseDeltaData = {
                    sagaId: deltaData.sagaId,
                    stage: SyncStage.delta,
                    proposeCommit: true,
                    payloadAddrs: [], // Always include empty array if sending delta
                };
                const deltaStone = await this.createSyncMsgStone({
                    data: responseDeltaData,
                    space: tempSpace,
                    metaspace
                });
                const deltaFrame = await this.evolveSyncSagaIbGib({
                    prevSagaIbGib: sagaIbGib,
                    msgStones: [deltaStone],
                    identity,
                    space: tempSpace,
                    metaspace
                });
                // IMMEDIATELY persist to both spaces for audit trail
                await this.ensureSagaFrameInBothSpaces({ frame: deltaFrame, destSpace, tempSpace, metaspace });
                // Check if PEER proposed commit
                if (deltaData.proposeCommit) {
                    if (logalot) {
                        console.log(`${lc} Peer proposed commit. Accepting & Committing.`);
                    }
                    // Peer wants to commit and has no more requests.
                    // We should Commit.
                    const commitData = {
                        sagaId: deltaData.sagaId,
                        stage: SyncStage.commit,
                        success: true,
                    };
                    const commitStone = await this.createSyncMsgStone({
                        data: commitData,
                        space: tempSpace,
                        metaspace
                    });
                    const commitFrame = await this.evolveSyncSagaIbGib({
                        prevSagaIbGib: deltaFrame, // Build on top of the Delta we just created/persisted
                        msgStones: [commitStone],
                        identity,
                        space: tempSpace,
                        metaspace
                    });
                    // IMMEDIATELY persist to both spaces for audit trail
                    await this.ensureSagaFrameInBothSpaces({ frame: commitFrame, destSpace, tempSpace, metaspace });
                    // Build control payloads for commit
                    const commitCtrlPayloads2 = [commitFrame, commitStone];
                    if (identity) {
                        commitCtrlPayloads2.push(identity);
                    }
                    return { frame: commitFrame, payloadIbGibsControl: commitCtrlPayloads2 };
                }
                // Build control payloads for delta propose
                const deltaCtrlPayloads = [deltaFrame, deltaStone];
                if (identity) {
                    deltaCtrlPayloads.push(identity);
                }
                return { frame: deltaFrame, payloadIbGibsControl: deltaCtrlPayloads };
            }
        }
    }
    async handleCommitFrame({ sagaIbGib, destSpace, tempSpace, metaspace, identity, }) {
        const lc = `${this.lc}[${this.handleCommitFrame.name}]`;
        if (logalot) {
            console.log(`${lc} Commit received.`);
        }
        // Sender Logic (Finalizing):
        // If we are here, we received a Commit frame from the Peer.
        // This implies the Peer has successfully committed.
        // We should now:
        // 1. Validate (implicitly done by receiving valid frame)
        // 2. Perform our own cleanup (Temp -> Dest, if applicable)
        // 3. Return null to signal saga completion.
        // Note: Currently we don't have explicit cleanup logic implemented here yet (TODO).
        if (logalot) {
            console.log(`${lc} Peer committed. Finalizing saga locally. Saga Complete.`);
        }
        return null;
    }
    // #endregion Handlers
    async createSyncMsgStone({ data, space, metaspace, }) {
        const lc = `${this.lc}[${this.createSyncMsgStone.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: 5f7f98e8ff980364f7191fcee4531e26)`);
            }
            const ib = await getSyncSagaMessageIb({ data });
            const stone = await Factory_V1.stone({
                ib,
                parentPrimitiveIb: SYNC_SAGA_MSG_ATOM,
                data,
                uuid: true, // we want the stone to have its own uniqueness
            });
            if (logalot) {
                console.log(`${lc} Created stone: ${getIbGibAddr({ ibGib: stone })}`);
            }
            await putInSpace({ space, ibGib: stone });
            await metaspace.registerNewIbGib({ ibGib: stone });
            return stone;
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    /**
     * Ensures saga frame and its msg stone(s) are in BOTH spaces for audit trail.
     * Control ibgibs (saga frames, msg stones, identity) must be in both destSpace and tempSpace.
     */
    async ensureSagaFrameInBothSpaces({ frame, destSpace, tempSpace, metaspace, }) {
        // Frame itself (already in tempSpace from creation, need in destSpace for audit)
        await putInSpace({ space: destSpace, ibGib: frame });
        await metaspace.registerNewIbGib({ ibGib: frame });
        // Msg stone(s) (already in tempSpace, need in destSpace for audit)
        const msgStoneAddrs = frame.rel8ns?.[SYNC_MSG_REL8N_NAME];
        if (msgStoneAddrs && msgStoneAddrs.length > 0) {
            const resMsgStones = await getFromSpace({ space: tempSpace, addrs: msgStoneAddrs });
            if (resMsgStones.ibGibs) {
                for (const msgStone of resMsgStones.ibGibs) {
                    await putInSpace({ space: destSpace, ibGib: msgStone });
                    await metaspace.registerNewIbGib({ ibGib: msgStone });
                }
            }
        }
        // Identity (if present, already in tempSpace, need in destSpace)
        const identityAddrs = frame.rel8ns?.identity;
        if (identityAddrs && identityAddrs.length > 0) {
            const resIdentity = await getFromSpace({ space: tempSpace, addrs: identityAddrs });
            if (resIdentity.ibGibs) {
                for (const identity of resIdentity.ibGibs) {
                    await putInSpace({ space: destSpace, ibGib: identity });
                    await metaspace.registerNewIbGib({ ibGib: identity });
                }
            }
        }
    }
    /**
     * Evolves the saga timeline with a new frame.
     */
    async evolveSyncSagaIbGib({ prevSagaIbGib, msgStones, identity, space, metaspace, conflictStrategy, }) {
        const lc = `${this.lc}[${this.evolveSyncSagaIbGib.name}]`;
        try {
            // Validation
            if (!msgStones || msgStones.length === 0) {
                throw new Error(`${lc} msgStones required (E: d13f8afcfb8816aeb26baae7df3ef726)`);
            }
            const firstStoneData = msgStones[0].data;
            if (!firstStoneData?.sagaId || !firstStoneData?.stage) {
                throw new Error(`${lc} Invalid stone data (missing sagaId or stage) (E: c004f85ce8f8958b11839f8d68c38826)`);
            }
            const sagaId = firstStoneData.sagaId;
            const stage = firstStoneData.stage;
            // Ensure all stones match sagaId and stage
            for (const stone of msgStones) {
                const d = stone.data;
                if (d.sagaId !== sagaId) {
                    throw new Error(`${lc} Mismatched sagaId in stones. Expected ${sagaId}, got ${d.sagaId} (E: 29e02719e426ecbbf8ee66887a2be226)`);
                }
                if (d.stage !== stage) {
                    throw new Error(`${lc} Mismatched stage in stones. Expected ${stage}, got ${d.stage} (E: d12c6571b0882f762921b60880c3f826)`);
                }
            }
            const identityAddr = identity ? getIbGibAddr({ ibGib: identity }) : undefined;
            if (prevSagaIbGib) {
                /**
                 * rel8ns always include the new msg stone(s)
                 */
                const rel8nInfos = [
                    {
                        rel8nName: SYNC_MSG_REL8N_NAME,
                        ibGibs: msgStones,
                    }
                ];
                // if we're authenticating/signing, we'll have identity
                if (identity) {
                    rel8nInfos.push({ rel8nName: 'identity', ibGibs: [identity], });
                }
                // remove the existing sync msg stones' addrs 
                if (!prevSagaIbGib.rel8ns) {
                    throw new Error(`(UNEXPECTED) prevSagaIbGib.rel8ns falsy? (E: 81375841aff85b1e48ea42ca218e6826)`);
                }
                if (!prevSagaIbGib.rel8ns[SYNC_MSG_REL8N_NAME] || prevSagaIbGib.rel8ns[SYNC_MSG_REL8N_NAME].length === 0) {
                    throw new Error(`(UNEXPECTED) prevSagaIbGib.rel8ns[SYNC_MSG_REL8N_NAME] falsy/empty? (E: 15156baad26fcccda80aa3a31718c726)`);
                }
                const rel8nRemovalInfos = [
                    {
                        rel8nName: SYNC_MSG_REL8N_NAME,
                        ibGibAddrs: prevSagaIbGib.rel8ns[SYNC_MSG_REL8N_NAME],
                    }
                ];
                // Append to existing timeline
                const resAppend = await appendToTimeline({
                    timeline: prevSagaIbGib,
                    rel8nInfos,
                    rel8nRemovalInfos,
                    metaspace,
                    space,
                    noDna: true, // Explicitly no DNA for sync frames
                });
                const newFrame = resAppend;
                return newFrame;
            }
            else {
                // Create New Timeline (Root Frame)
                const data = {
                    uuid: sagaId,
                    // stage, // Removed from V1
                    payload: undefined, // Data in stone
                    n: 0,
                    isTjp: true,
                    conflictStrategy,
                };
                const ib = await getSyncIb({ data });
                const stoneAddrs = msgStones.map(s => getIbGibAddr({ ibGib: s }));
                const rel8ns = {
                    [SYNC_MSG_REL8N_NAME]: stoneAddrs,
                };
                if (identityAddr) {
                    rel8ns.identity = [identityAddr];
                }
                const resNew = await createTimeline({
                    space,
                    metaspace,
                    ib,
                    data,
                    rel8ns,
                    parentIb: SYNC_ATOM, // "sync"
                    noDna: true,
                });
                return resNew.newIbGib;
            }
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
    }
    async getStageAndPayloadFromFrame({ ibGib, space }) {
        const lc = `${this.lc}[${this.getStageAndPayloadFromFrame.name}]`;
        try {
            if (logalot) {
                console.log(`${lc} starting... (I: fddc287bd4d55253dc50e519fd352226)`);
            }
            if (!ibGib.rel8ns) {
                throw new Error(`(UNEXPECTED) ibGib.rel8ns falsy? (E: 725bc8eb5dfe5c7058907ad8e3063826)`);
            }
            const stoneAddrs = ibGib.rel8ns?.[SYNC_MSG_REL8N_NAME];
            if (stoneAddrs && stoneAddrs.length > 0) {
                const stoneAddr = stoneAddrs[0];
                const res = await getFromSpace({ addr: stoneAddr, space });
                if (res.ibGibs && res.ibGibs.length > 0) {
                    const stone = res.ibGibs[0];
                    if (!stone.data) {
                        throw new Error(`(UNEXPECTED) message stone.data falsy? (E: 4d4abcea90d87ba1328434e83d0d0826)`);
                    }
                    if (!stone.data.stage) {
                        throw new Error(`(UNEXPECTED) message stone.data.stage falsy? (E: 72c8d22310996644ecdd2b5fdb288c26)`);
                    }
                    return { stage: stone.data.stage, messageData: stone.data };
                }
                else {
                    throw new Error(`couldn't get stoneAddr (${stoneAddr}) from space (${space.ib}) (E: f3c826e756f8a5c358beb88aa4a65e26)`);
                }
            }
            else {
                throw new Error(`(UNEXPECTED) no stone addrs rel8d to sync saga ibgib frame? ibGib addr: ${getIbGibAddr(ibGib)} (E: 43eae8579e289d016741b5526052f226)`);
            }
        }
        catch (error) {
            console.error(`${lc} ${extractErrorMsg(error)}`);
            throw error;
        }
        finally {
            if (logalot) {
                console.log(`${lc} complete.`);
            }
        }
    }
    sortTimelinesTopologically(timelines) {
        const lc = `${this.lc}[${this.sortTimelinesTopologically.name}]`;
        const tjps = Object.keys(timelines);
        if (tjps.length === 0) {
            return [];
        }
        // 1. Build a reverse lookup: ibGibAddr -> tjpAddr
        const addrToTjp = {};
        tjps.forEach(tjp => {
            timelines[tjp].forEach(ibGib => {
                const addr = getIbGibAddr({ ibGib });
                addrToTjp[addr] = tjp;
            });
        });
        // 2. Build adjacency list for timelines: tjp -> Set<dependencyTjp>
        // Edge direction: T1 -> T2 means T1 depends on T2.
        const adj = {};
        tjps.forEach(tjp => adj[tjp] = new Set());
        tjps.forEach(tjp => {
            timelines[tjp].forEach(ibGib => {
                if (ibGib.rel8ns) {
                    Object.values(ibGib.rel8ns).flat().forEach(relAddr => {
                        if (!relAddr) {
                            return;
                        }
                        const depTjp = addrToTjp[relAddr];
                        // If dependency is in our scope and is a different timeline
                        if (depTjp && depTjp !== tjp) {
                            adj[tjp].add(depTjp);
                        }
                    });
                }
            });
        });
        // 3. Topological Sort (DFS)
        const visited = new Set();
        const temp = new Set(); // for cycle detection
        const sorted = [];
        const visit = (node) => {
            if (temp.has(node)) {
                console.warn(`${lc} Cycle detected involving ${node}. Ignoring edge to break cycle. (W: 7e7352347102431aa5318db962459929)`);
                return;
            }
            if (visited.has(node))
                return;
            temp.add(node);
            // Visit dependencies first
            adj[node].forEach(dep => visit(dep));
            visited.add(node);
            temp.delete(node);
            sorted.push(node);
        };
        tjps.forEach(node => {
            if (!visited.has(node))
                visit(node);
        });
        return sorted;
    }
}
//# sourceMappingURL=sync-saga-coordinator.mjs.map