/**
 * SessionMigrator — Orchestrates session halt, account switch, and restart
 * when quota approaches reserve thresholds.
 *
 * Ported from Dawn's dawn-server equivalent for general Instar use.
 * Adapted to use Instar's SessionManager, StateManager, and EventEmitter.
 *
 * Migration flow:
 * 1. Pause scheduler (prevent new spawns)
 * 2. Find best alternative account
 * 3. Gracefully halt running sessions (Ctrl+C → wait → kill)
 * 4. Switch account credentials
 * 5. Restart halted sessions on new account
 * 6. Resume scheduler
 *
 * Part of the Instar Quota Migration spec (Phase 3).
 */
import { EventEmitter } from 'node:events';
import fs from 'node:fs';
import path from 'node:path';
import { DegradationReporter } from './DegradationReporter.js';
import { SafeFsExecutor } from '../core/SafeFsExecutor.js';
// ── Constants ────────────────────────────────────────────────────────
const DEFAULT_THRESHOLDS = {
    fiveHourPercent: 88,
    weeklyPercent: 92,
    cooldownMs: 10 * 60 * 1000, // 10 minutes
    minimumHeadroom: 20,
    gracePeriodMs: 5000,
};
const LOCK_STALE_MS = 10 * 60 * 1000; // 10 minutes
const MAX_HISTORY = 50;
const ACCOUNT_SETTLE_MS = 2000; // Brief pause after account switch
// ── Implementation ───────────────────────────────────────────────────
export class SessionMigrator extends EventEmitter {
    deps = null;
    thresholds;
    stateDir;
    lockPath;
    statePath;
    historyPath;
    migrationState = null;
    constructor(config) {
        super();
        this.stateDir = config.stateDir;
        this.thresholds = { ...DEFAULT_THRESHOLDS, ...config.thresholds };
        this.lockPath = path.join(this.stateDir, 'migration.lock');
        this.statePath = path.join(this.stateDir, 'migration-state.json');
        this.historyPath = path.join(this.stateDir, 'migration-history.json');
        // Ensure state directory exists
        if (!fs.existsSync(this.stateDir)) {
            fs.mkdirSync(this.stateDir, { recursive: true });
        }
        // Check for incomplete migration from a crash
        this.recoverFromCrash();
    }
    /**
     * Set dependencies for integration with SessionManager and JobScheduler.
     * Must be called before checkAndMigrate().
     */
    setDeps(deps) {
        this.deps = deps;
    }
    /**
     * Main entry point — called after each quota refresh.
     * Checks if migration is needed and triggers it if so.
     */
    async checkAndMigrate(quotaState) {
        if (!this.deps) {
            return false;
        }
        // Don't trigger if migration is in progress
        if (this.migrationState && this.migrationState.status !== 'idle' &&
            this.migrationState.status !== 'complete' && this.migrationState.status !== 'failed') {
            return false;
        }
        // Check cooldown — but bypass for escalation from enforced_pause to enforced_kill.
        // If the last event was a 90% warning and quota has risen to 95%+, the kill
        // must fire immediately regardless of cooldown.
        const history = this.loadHistory();
        if (history.lastMigration) {
            const elapsed = Date.now() - new Date(history.lastMigration.completedAt).getTime();
            if (elapsed < this.thresholds.cooldownMs) {
                const fiveHour = quotaState.fiveHourPercent ?? 0;
                const lastWasPause = history.lastMigration.result === 'enforced_pause';
                const nowCritical = fiveHour >= 95;
                if (!(lastWasPause && nowCritical)) {
                    return false;
                }
                // Allow escalation from pause to kill
            }
        }
        // Check if migration is needed
        const reason = this.shouldMigrate(quotaState);
        if (!reason) {
            return false;
        }
        return await this.executeMigration(quotaState.activeAccountEmail || null, reason, quotaState.fiveHourPercent ?? 0);
    }
    /**
     * Determine if migration should be triggered.
     */
    shouldMigrate(quotaState) {
        // Check 5-hour rate limit (most urgent — causes immediate failures)
        const fiveHour = quotaState.fiveHourPercent;
        if (fiveHour != null && fiveHour >= this.thresholds.fiveHourPercent) {
            return `5-hour rate limit at ${fiveHour}% (threshold: ${this.thresholds.fiveHourPercent}%)`;
        }
        // Check weekly budget
        if (quotaState.percentUsed >= this.thresholds.weeklyPercent) {
            return `weekly quota at ${quotaState.percentUsed}% (threshold: ${this.thresholds.weeklyPercent}%)`;
        }
        return null;
    }
    /**
     * Execute the full migration flow with rollback support.
     */
    async executeMigration(activeAccountEmail, reason, fiveHourPercent = 0) {
        // Acquire lock
        if (!this.acquireLock()) {
            return false;
        }
        const startTime = Date.now();
        const event = {
            triggeredAt: new Date().toISOString(),
            reason,
            previousAccount: activeAccountEmail,
            newAccount: null,
            sessionsHalted: [],
            sessionsRestarted: [],
            result: 'failed',
            completedAt: '',
            durationMs: 0,
        };
        try {
            // Step 1: Find best alternative account
            const target = this.selectMigrationTarget();
            if (!target) {
                // No alternative account — enforce quota protection based on severity.
                // The entire purpose of the quota system is to prevent hitting the limit,
                // so we MUST enforce, not just silently record.
                if (fiveHourPercent >= 95) {
                    // CRITICAL: 95%+ — absolute stop. Kill everything to preserve quota.
                    event.result = 'enforced_kill';
                    event.completedAt = new Date().toISOString();
                    event.durationMs = Date.now() - startTime;
                    this.deps.pauseScheduler();
                    const killed = await this.haltRunningSessions();
                    event.sessionsHalted = killed.map(s => s.jobSlug || s.name);
                    this.emit('enforced_kill', {
                        reason,
                        fiveHourPercent,
                        sessionsKilled: event.sessionsHalted,
                    });
                    this.recordMigration(event);
                    return false;
                }
                else if (fiveHourPercent >= 90) {
                    // WARNING: 90%+ — send graceful shutdown to sessions, pause scheduler.
                    event.result = 'enforced_pause';
                    event.completedAt = new Date().toISOString();
                    event.durationMs = Date.now() - startTime;
                    // Send Ctrl+C to all running sessions
                    const sessions = this.deps.listRunningSessions();
                    for (const session of sessions) {
                        try {
                            this.deps.sendKey(session.tmuxSession, 'C-c');
                        }
                        catch { /* @silent-fallback-ok — best-effort signal, session will be killed at 95% if still running */ }
                    }
                    this.deps.pauseScheduler();
                    this.emit('enforced_pause', {
                        reason,
                        fiveHourPercent,
                        sessionsSignaled: sessions.length,
                    });
                    this.recordMigration(event);
                    return false;
                }
                else {
                    // Below 90% — warn but don't enforce yet
                    event.result = 'no_alternative';
                    event.completedAt = new Date().toISOString();
                    event.durationMs = Date.now() - startTime;
                    this.emit('migration_no_target', {
                        reason,
                        sourceAccount: activeAccountEmail || 'unknown',
                    });
                    this.recordMigration(event);
                    return false;
                }
            }
            event.newAccount = target.email;
            // Initialize migration state
            this.migrationState = {
                status: 'halting',
                startedAt: new Date().toISOString(),
                sourceAccount: activeAccountEmail || 'unknown',
                targetAccount: target.email,
                haltedSessions: [],
                restartedSessions: [],
            };
            this.saveMigrationState();
            this.emit('state_changed', { ...this.migrationState });
            // Step 2: Emit start event
            this.emit('migration_started', {
                reason,
                sourceAccount: activeAccountEmail || 'unknown',
                targetAccount: target.email,
            });
            // Step 3: Pause scheduler
            this.deps.pauseScheduler();
            // Step 4: Gracefully halt running sessions
            const haltedSessions = await this.haltRunningSessions();
            event.sessionsHalted = haltedSessions.map(s => s.jobSlug || s.name);
            this.migrationState.haltedSessions = haltedSessions.map(s => ({
                sessionId: s.id,
                jobSlug: s.jobSlug || s.name,
                haltedAt: new Date().toISOString(),
            }));
            this.saveMigrationState();
            // Step 5: Switch account
            this.migrationState.status = 'switching';
            this.saveMigrationState();
            this.emit('state_changed', { ...this.migrationState });
            const switchResult = await this.deps.switchAccount(target.email);
            if (!switchResult.success) {
                // Rollback: halt succeeded but switch failed — restart on original account
                this.migrationState.status = 'rolling_back';
                this.migrationState.error = `Account switch failed: ${switchResult.message}`;
                this.saveMigrationState();
                this.emit('state_changed', { ...this.migrationState });
                await this.rollbackRestartSessions(haltedSessions);
                this.deps.resumeScheduler();
                event.result = 'rolled_back';
                event.error = switchResult.message;
                event.completedAt = new Date().toISOString();
                event.durationMs = Date.now() - startTime;
                this.emit('migration_rollback', event);
                this.recordMigration(event);
                return false;
            }
            // Step 6: Brief pause to let credentials settle
            await this.sleep(ACCOUNT_SETTLE_MS);
            // Step 7: Restart halted sessions on new account
            this.migrationState.status = 'restarting';
            this.saveMigrationState();
            this.emit('state_changed', { ...this.migrationState });
            const restartedSlugs = [];
            for (const session of haltedSessions) {
                const slug = session.jobSlug || session.name;
                try {
                    await this.deps.respawnJob(slug);
                    restartedSlugs.push(slug);
                    this.migrationState.restartedSessions.push({
                        sessionId: session.id,
                        jobSlug: slug,
                        startedAt: new Date().toISOString(),
                    });
                    this.saveMigrationState();
                }
                catch (err) {
                    console.error(`[SessionMigrator] Failed to restart ${slug}:`, err);
                }
            }
            event.sessionsRestarted = restartedSlugs;
            // Step 8: Resume scheduler
            this.deps.resumeScheduler();
            // Step 9: Determine result
            if (restartedSlugs.length === haltedSessions.length) {
                event.result = 'success';
                this.migrationState.status = 'complete';
            }
            else {
                event.result = 'partial';
                this.migrationState.status = 'complete';
                const failedSlugs = haltedSessions
                    .map(s => s.jobSlug || s.name)
                    .filter(slug => !restartedSlugs.includes(slug));
                this.migrationState.error = `Failed to restart: ${failedSlugs.join(', ')}`;
            }
            event.completedAt = new Date().toISOString();
            event.durationMs = Date.now() - startTime;
            this.saveMigrationState();
            // Step 10: Emit completion
            if (event.result === 'success') {
                this.emit('migration_complete', event);
            }
            else {
                this.emit('migration_partial', event);
            }
            this.recordMigration(event);
            return true;
        }
        catch (err) {
            DegradationReporter.getInstance().report({
                feature: 'SessionMigrator.executeMigration',
                primary: 'Migrate sessions to alternative account on quota exhaustion',
                fallback: 'Migration failed, sessions may remain on exhausted account',
                reason: `Migration error: ${err instanceof Error ? err.message : String(err)}`,
                impact: 'Sessions may hit rate limits until migration succeeds or quota resets',
            });
            console.error('[SessionMigrator] Migration error:', err);
            event.result = 'failed';
            event.error = err instanceof Error ? err.message : String(err);
            event.completedAt = new Date().toISOString();
            event.durationMs = Date.now() - startTime;
            if (this.migrationState) {
                this.migrationState.status = 'failed';
                this.migrationState.error = event.error;
                this.saveMigrationState();
            }
            // Ensure scheduler is resumed even on error
            try {
                this.deps.resumeScheduler();
            }
            catch { // @silent-fallback-ok — best-effort scheduler resume after migration failure
            }
            this.emit('migration_failed', event);
            this.recordMigration(event);
            return false;
        }
        finally {
            this.releaseLock();
        }
    }
    /**
     * Gracefully halt running sessions:
     * 1. Send Ctrl+C to each session
     * 2. Wait grace period for them to flush state
     * 3. Kill any that are still alive
     */
    async haltRunningSessions() {
        if (!this.deps)
            return [];
        const sessions = this.deps.listRunningSessions();
        if (sessions.length === 0)
            return [];
        // Phase 0 (reap-notify spec R2.1): snapshot work evidence at THIS decision
        // moment — before Ctrl+C tears the work down. The quota-shed kills that
        // motivated mid-work tagging flow through here; a chokepoint re-check at
        // force-kill time would record "not working" for exactly these sessions.
        const evidenceBySession = new Map();
        for (const session of sessions) {
            const evidence = [];
            try {
                evidence.push(...(this.deps.collectWorkEvidence?.(session.id) ?? []));
            }
            catch {
                // evidence collection must never endanger the migration
            }
            try {
                if (this.deps.isBuildOrAutonomousActive?.(session.tmuxSession) &&
                    !evidence.includes('build-or-autonomous-active')) {
                    evidence.push('build-or-autonomous-active');
                }
            }
            catch {
                // same — observe-only
            }
            evidenceBySession.set(session.id, evidence);
        }
        // Phase 1: Send Ctrl+C to all sessions
        for (const session of sessions) {
            try {
                this.deps.sendKey(session.tmuxSession, 'C-c');
            }
            catch (err) {
                console.error(`[SessionMigrator] Failed to send C-c to ${session.tmuxSession}:`, err);
            }
        }
        // Phase 2: Wait for grace period
        await this.sleep(this.thresholds.gracePeriodMs);
        // §P0 #7 — bounded soft-check (SE-9): when current usage is at/below the
        // configured ceiling, build/autonomous-active sessions get ONE extra Ctrl+C
        // grace round before force-kill. Above the ceiling the soft check is
        // disabled — quota's final authority cannot be undermined when usage is
        // already near 100%/lockout.
        const softEnabled = this.thresholds.softCheckEnabled !== false;
        const softCeilingPct = this.thresholds.softCheckMaxUsagePercent ?? 95;
        const softExtraGraceMs = this.thresholds.softCheckExtraGraceMs ?? this.thresholds.gracePeriodMs;
        const currentUsagePct = this.deps.quotaUsagePercent?.() ?? 100;
        const softCheckActive = softEnabled && currentUsagePct <= softCeilingPct;
        // Phase 3: Kill any sessions still alive
        const halted = [];
        for (const session of sessions) {
            try {
                if (this.deps.isSessionAlive(session.tmuxSession)) {
                    // Soft check: give a working session one more chance before force-kill.
                    if (softCheckActive && this.deps.isBuildOrAutonomousActive?.(session.tmuxSession)) {
                        console.log(`[SessionMigrator] "${session.tmuxSession}": active build/autonomous run — `
                            + `granting one extra ${Math.round(softExtraGraceMs / 1000)}s Ctrl+C grace before force-kill `
                            + `(usage ${currentUsagePct}% ≤ ceiling ${softCeilingPct}%)`);
                        try {
                            this.deps.sendKey(session.tmuxSession, 'C-c');
                        }
                        catch { /* @silent-fallback-ok */ }
                        await this.sleep(softExtraGraceMs);
                        if (!this.deps.isSessionAlive(session.tmuxSession)) {
                            halted.push(session);
                            continue;
                        }
                    }
                    // Emit the structural force-kill decision so a tier1 supervisor (LLM
                    // Haiku wrap, future) can observe inputs + outcome. The kill still
                    // happens — this is observability, not a gate.
                    this.emit('quota-force-kill-decision', {
                        tmuxSession: session.tmuxSession,
                        sessionId: session.id,
                        jobSlug: session.jobSlug,
                        currentUsagePct,
                        softCheckActive,
                        workingSoftCheckFired: softCheckActive && (this.deps.isBuildOrAutonomousActive?.(session.tmuxSession) ?? false),
                    });
                    // §P0 #7 route the force-kill through the single ReapAuthority when
                    // available (the §P3 notifier surfaces it; the reap-log records the
                    // `quota-shed` reason). Falls back to the raw killSession when the
                    // dep is unwired (older agents).
                    if (this.deps.terminateSession) {
                        const result = await this.deps.terminateSession(session.id, 'quota-shed', {
                            disposition: 'terminal',
                            finalStatus: 'killed',
                            // Killer-supplied evidence from the pre-grace snapshot (R2.1).
                            workEvidence: evidenceBySession.get(session.id) ?? [],
                        });
                        // Refusal recording (reap-notify foundation fix): a ReapGuard
                        // refusal means the session is REFUSED-AND-ALIVE — counting it as
                        // halted both lies in the migration record and double-respawns its
                        // job downstream (the restart loop spawns a second copy alongside
                        // the survivor). Skip it, loudly.
                        if (!result.terminated) {
                            console.warn(`[SessionMigrator] terminate REFUSED for ${session.tmuxSession} ` +
                                `(${result.skipped ?? 'no reason'}) — recording as refused, NOT halted`);
                            this.emit('halt-refused', {
                                sessionId: session.id,
                                tmuxSession: session.tmuxSession,
                                jobSlug: session.jobSlug,
                                skipped: result.skipped,
                            });
                            continue;
                        }
                    }
                    else {
                        this.deps.killSession(session.id);
                    }
                }
                halted.push(session);
            }
            catch (err) {
                console.error(`[SessionMigrator] Failed to kill ${session.tmuxSession}:`, err);
                // Still record it as halted — it may have died from Ctrl+C
                halted.push(session);
            }
        }
        return halted;
    }
    /**
     * Rollback: restart sessions on the original account after a failed switch.
     */
    async rollbackRestartSessions(sessions) {
        if (!this.deps)
            return;
        for (const session of sessions) {
            const slug = session.jobSlug || session.name;
            try {
                await this.deps.respawnJob(slug);
            }
            catch (err) {
                DegradationReporter.getInstance().report({
                    feature: 'SessionMigrator.rollbackRestartSessions',
                    primary: `Restart session ${slug} during migration rollback`,
                    fallback: `Session ${slug} left halted after failed rollback`,
                    reason: `Rollback restart failed: ${err instanceof Error ? err.message : String(err)}`,
                    impact: `Job ${slug} is not running and requires manual restart`,
                });
                console.error(`[SessionMigrator] Rollback restart failed for ${slug}:`, err);
            }
        }
    }
    /**
     * Select the best account to migrate to.
     *
     * Algorithm:
     * 1. Filter: Exclude stale, expired, current, and accounts without tokens
     * 2. Filter: Must have minimum headroom (default 20%)
     * 3. Score: 100 - weeklyPercent (lower usage = higher score)
     * 4. Tiebreak: Prefer account with later weeklyResetsAt
     */
    selectMigrationTarget() {
        if (!this.deps)
            return null;
        const accounts = this.deps.getAccountStatuses();
        const maxUsage = 100 - this.thresholds.minimumHeadroom;
        const candidates = accounts
            .filter(a => !a.isActive && !a.isStale && a.hasToken && !a.tokenExpired)
            .filter(a => a.weeklyPercent <= maxUsage)
            .filter(a => {
            // Also check 5-hour rate limit has headroom
            if (a.fiveHourPercent != null && a.fiveHourPercent >= 70)
                return false;
            return true;
        })
            .sort((a, b) => {
            // Primary: lowest weekly usage
            if (a.weeklyPercent !== b.weeklyPercent) {
                return a.weeklyPercent - b.weeklyPercent;
            }
            // Tiebreak: later reset time (more time before reset)
            const aReset = a.weeklyResetsAt ? new Date(a.weeklyResetsAt).getTime() : 0;
            const bReset = b.weeklyResetsAt ? new Date(b.weeklyResetsAt).getTime() : 0;
            return bReset - aReset;
        });
        return candidates[0] ?? null;
    }
    // ── Lock Management ─────────────────────────────────────────────────
    acquireLock() {
        try {
            if (fs.existsSync(this.lockPath)) {
                const lockData = JSON.parse(fs.readFileSync(this.lockPath, 'utf-8'));
                const lockAge = Date.now() - new Date(lockData.acquiredAt).getTime();
                if (lockAge < LOCK_STALE_MS) {
                    console.log(`[SessionMigrator] Lock held by pid ${lockData.pid}, age ${Math.round(lockAge / 1000)}s — skipping`);
                    return false;
                }
                console.log(`[SessionMigrator] Stale lock (${Math.round(lockAge / 1000)}s old) — taking over`);
            }
            fs.writeFileSync(this.lockPath, JSON.stringify({
                pid: process.pid,
                acquiredAt: new Date().toISOString(),
            }), { mode: 0o600 });
            return true;
        }
        catch (err) {
            console.error('[SessionMigrator] Failed to acquire lock:', err);
            return false;
        }
    }
    releaseLock() {
        try {
            if (fs.existsSync(this.lockPath)) {
                SafeFsExecutor.safeUnlinkSync(this.lockPath, { operation: 'src/monitoring/SessionMigrator.ts:632' });
            }
        }
        catch {
            // @silent-fallback-ok — lock may have been cleaned up already
        }
    }
    // ── Crash Recovery ──────────────────────────────────────────────────
    /**
     * On startup, check for incomplete migration state.
     * If found, attempt rollback by restarting halted sessions on the source account.
     */
    recoverFromCrash() {
        const state = this.loadMigrationState();
        if (!state)
            return;
        if (state.status === 'idle' || state.status === 'complete' || state.status === 'failed') {
            return;
        }
        console.warn(`[SessionMigrator] Found incomplete migration (status: ${state.status}). Will attempt recovery when deps are set.`);
        this.migrationState = state;
    }
    /**
     * Called after deps are set to complete crash recovery if needed.
     * Should be called once after setDeps().
     */
    async completeRecovery() {
        if (!this.migrationState || !this.deps)
            return;
        const state = this.migrationState;
        if (state.status === 'idle' || state.status === 'complete' || state.status === 'failed') {
            return;
        }
        console.log(`[SessionMigrator] Recovering from interrupted migration (status: ${state.status})`);
        // For any non-terminal state, try to restart halted sessions that weren't restarted
        const restartedIds = new Set(state.restartedSessions.map(s => s.sessionId));
        const needsRestart = state.haltedSessions.filter(s => !restartedIds.has(s.sessionId));
        if (needsRestart.length > 0) {
            console.log(`[SessionMigrator] Restarting ${needsRestart.length} sessions from crash recovery`);
            for (const session of needsRestart) {
                try {
                    await this.deps.respawnJob(session.jobSlug);
                }
                catch (err) {
                    console.error(`[SessionMigrator] Recovery restart failed for ${session.jobSlug}:`, err);
                }
            }
        }
        // Mark recovery complete
        this.migrationState = {
            status: 'complete',
            sourceAccount: state.sourceAccount,
            targetAccount: state.targetAccount,
            haltedSessions: state.haltedSessions,
            restartedSessions: state.restartedSessions,
            error: `Recovered from crash (previous status: ${state.status})`,
        };
        this.saveMigrationState();
        // Resume scheduler if it was left paused
        try {
            this.deps.resumeScheduler();
        }
        catch { // @silent-fallback-ok — best-effort scheduler resume during crash recovery
        }
    }
    // ── State Persistence ───────────────────────────────────────────────
    saveMigrationState() {
        try {
            fs.writeFileSync(this.statePath, JSON.stringify(this.migrationState, null, 2), { mode: 0o600 });
        }
        catch (err) {
            DegradationReporter.getInstance().report({
                feature: 'SessionMigrator.saveMigrationState',
                primary: 'Persist migration state to disk for crash recovery',
                fallback: 'Migration state exists in memory only',
                reason: `Write failed: ${err instanceof Error ? err.message : String(err)}`,
                impact: 'Crash recovery will not be able to resume this migration',
            });
            console.error('[SessionMigrator] Failed to save migration state:', err);
        }
    }
    loadMigrationState() {
        try {
            if (!fs.existsSync(this.statePath))
                return null;
            return JSON.parse(fs.readFileSync(this.statePath, 'utf-8'));
        }
        catch {
            // @silent-fallback-ok — corrupt state file treated as no state; fresh start is safe
            return null;
        }
    }
    loadHistory() {
        try {
            if (!fs.existsSync(this.historyPath)) {
                return { lastMigration: null, migrations: [] };
            }
            return JSON.parse(fs.readFileSync(this.historyPath, 'utf-8'));
        }
        catch {
            return { lastMigration: null, migrations: [] };
        }
    }
    recordMigration(event) {
        const history = this.loadHistory();
        history.lastMigration = event;
        history.migrations.push(event);
        if (history.migrations.length > MAX_HISTORY) {
            history.migrations = history.migrations.slice(-MAX_HISTORY);
        }
        try {
            fs.writeFileSync(this.historyPath, JSON.stringify(history, null, 2), { mode: 0o600 });
        }
        catch (err) {
            console.error('[SessionMigrator] Failed to save migration history:', err);
        }
    }
    // ── Public API ──────────────────────────────────────────────────────
    /**
     * Get current migration status for API/monitoring.
     */
    getMigrationStatus() {
        const history = this.loadHistory();
        return {
            inProgress: this.migrationState != null &&
                this.migrationState.status !== 'idle' &&
                this.migrationState.status !== 'complete' &&
                this.migrationState.status !== 'failed',
            state: this.migrationState,
            lastMigration: history.lastMigration,
            history: history.migrations,
            config: { ...this.thresholds },
        };
    }
    /**
     * Is migration currently in progress?
     */
    isMigrating() {
        return this.migrationState != null &&
            this.migrationState.status !== 'idle' &&
            this.migrationState.status !== 'complete' &&
            this.migrationState.status !== 'failed';
    }
    /**
     * Get thresholds (for external monitoring/display).
     */
    getThresholds() {
        return { ...this.thresholds };
    }
    // ── Helpers ─────────────────────────────────────────────────────────
    sleep(ms) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }
}
//# sourceMappingURL=SessionMigrator.js.map