/**
 * Session Lanes for Tool Execution
 *
 * OpenClaw-inspired serialized execution system to prevent race conditions:
 * - Per-session lane serialization
 * - Global lane for cross-session operations
 * - Timeout handling with configurable limits
 * - Early termination via AbortSignal
 * - Queue management and metrics
 */
import { EventEmitter } from 'events';
import { logger } from '../../utils/logger.js';
/**
 * Default configuration
 */
export const DEFAULT_SESSION_LANES_CONFIG = {
    defaultTimeoutMs: 600_000, // 10 minutes (OpenClaw default)
    defaultWaitTimeoutMs: 30_000, // 30 seconds (OpenClaw default)
    maxQueueSize: 100,
    laneIdleTimeoutMs: 300_000, // 5 minutes
    enableMetrics: true,
    globalLaneConcurrency: 1,
};
// ============================================================================
// Session Lane
// ============================================================================
/**
 * Individual session lane for serialized execution
 */
class SessionLane extends EventEmitter {
    config;
    sessionId;
    queue = [];
    status = 'idle';
    currentTask = null;
    totalExecuted = 0;
    totalErrors = 0;
    createdAt;
    lastActivityAt;
    processing = false;
    constructor(sessionId, config) {
        super();
        this.config = config;
        this.sessionId = sessionId;
        this.createdAt = Date.now();
        this.lastActivityAt = Date.now();
    }
    /**
     * Queue a task for execution
     */
    async enqueue(task) {
        if (this.queue.length >= this.config.maxQueueSize) {
            throw new Error(`Lane queue full (max ${this.config.maxQueueSize})`);
        }
        const taskId = `task_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
        return new Promise((resolve, reject) => {
            const fullTask = {
                ...task,
                id: taskId,
                createdAt: Date.now(),
                resolve: resolve,
                reject,
            };
            // Insert by priority (higher first)
            const insertIndex = this.queue.findIndex(t => t.priority < task.priority);
            if (insertIndex === -1) {
                this.queue.push(fullTask);
            }
            else {
                this.queue.splice(insertIndex, 0, fullTask);
            }
            this.lastActivityAt = Date.now();
            this.emit('task:queued', this.sessionId, taskId);
            // Start processing if not already
            this.processQueue();
        });
    }
    /**
     * Process the queue
     */
    async processQueue() {
        if (this.processing || this.queue.length === 0)
            return;
        this.processing = true;
        this.status = 'executing';
        while (this.queue.length > 0) {
            const task = this.queue.shift();
            this.currentTask = task;
            task.startedAt = Date.now();
            this.emit('task:started', this.sessionId, task.id);
            try {
                // Execute with timeout and abort signal
                const result = await this.executeWithTimeout(task);
                task.completedAt = Date.now();
                this.totalExecuted++;
                this.lastActivityAt = Date.now();
                const duration = task.completedAt - task.startedAt;
                this.emit('task:completed', this.sessionId, task.id, duration);
                task.resolve(result);
            }
            catch (error) {
                task.completedAt = Date.now();
                this.totalErrors++;
                this.lastActivityAt = Date.now();
                const err = error;
                if (err.message.includes('aborted') || err.message.includes('cancelled')) {
                    this.emit('task:cancelled', this.sessionId, task.id);
                }
                else if (err.message.includes('timeout')) {
                    this.emit('task:timeout', this.sessionId, task.id);
                }
                else {
                    this.emit('task:failed', this.sessionId, task.id, err);
                }
                task.reject(err);
            }
            this.currentTask = null;
        }
        this.processing = false;
        this.status = 'idle';
    }
    /**
     * Execute task with timeout and abort signal
     */
    async executeWithTimeout(task) {
        return new Promise((resolve, reject) => {
            // Set up timeout
            const timeoutId = setTimeout(() => {
                reject(new Error(`Task ${task.id} timed out after ${task.timeoutMs}ms`));
            }, task.timeoutMs);
            // Set up abort signal listener
            if (task.abortSignal) {
                if (task.abortSignal.aborted) {
                    clearTimeout(timeoutId);
                    reject(new Error(`Task ${task.id} was aborted`));
                    return;
                }
                task.abortSignal.addEventListener('abort', () => {
                    clearTimeout(timeoutId);
                    reject(new Error(`Task ${task.id} was cancelled`));
                });
            }
            // Execute task
            task.execute()
                .then((result) => {
                clearTimeout(timeoutId);
                resolve(result);
            })
                .catch((error) => {
                clearTimeout(timeoutId);
                reject(error);
            });
        });
    }
    /**
     * Cancel all pending tasks
     */
    cancelAll() {
        const count = this.queue.length;
        for (const task of this.queue) {
            task.reject(new Error('Task cancelled'));
            this.emit('task:cancelled', this.sessionId, task.id);
        }
        this.queue = [];
        return count;
    }
    /**
     * Cancel a specific task
     */
    cancelTask(taskId) {
        const index = this.queue.findIndex(t => t.id === taskId);
        if (index === -1)
            return false;
        const [task] = this.queue.splice(index, 1);
        task.reject(new Error('Task cancelled'));
        this.emit('task:cancelled', this.sessionId, taskId);
        return true;
    }
    /**
     * Drain the lane (wait for current task, cancel pending)
     */
    async drain() {
        this.status = 'draining';
        // Cancel pending tasks
        this.cancelAll();
        // Wait for current task to complete
        if (this.currentTask) {
            await new Promise((resolve) => {
                const checkInterval = setInterval(() => {
                    if (!this.currentTask) {
                        clearInterval(checkInterval);
                        resolve();
                    }
                }, 100);
            });
        }
        this.status = 'idle';
    }
    /**
     * Get lane info
     */
    getInfo() {
        return {
            sessionId: this.sessionId,
            status: this.status,
            queueSize: this.queue.length,
            currentTaskId: this.currentTask?.id,
            totalExecuted: this.totalExecuted,
            totalErrors: this.totalErrors,
            createdAt: this.createdAt,
            lastActivityAt: this.lastActivityAt,
        };
    }
    /**
     * Check if lane is idle and old enough for cleanup
     */
    isIdleForCleanup(idleTimeoutMs) {
        return (this.status === 'idle' &&
            this.queue.length === 0 &&
            Date.now() - this.lastActivityAt > idleTimeoutMs);
    }
}
// ============================================================================
// Session Lanes Manager
// ============================================================================
/**
 * Manages all session lanes
 */
export class SessionLanesManager extends EventEmitter {
    config;
    lanes = new Map();
    globalLane;
    cleanupInterval = null;
    constructor(config = {}) {
        super();
        this.config = { ...DEFAULT_SESSION_LANES_CONFIG, ...config };
        // Create global lane
        this.globalLane = new SessionLane('__global__', this.config);
        this.setupLaneEvents(this.globalLane);
        // Start cleanup interval
        this.startCleanupInterval();
    }
    /**
     * Execute a task in a session lane
     */
    async execute(sessionId, name, fn, options = {}) {
        const lane = options.useGlobalLane ? this.globalLane : this.getOrCreateLane(sessionId);
        const startTime = Date.now();
        try {
            const result = await lane.enqueue({
                sessionId,
                name,
                execute: fn,
                priority: options.priority ?? 100,
                timeoutMs: options.timeoutMs ?? this.config.defaultTimeoutMs,
                abortSignal: options.abortSignal,
            });
            return {
                taskId: `${sessionId}_${Date.now()}`,
                success: true,
                result,
                executionTimeMs: Date.now() - startTime,
                cancelled: false,
                timedOut: false,
            };
        }
        catch (error) {
            const err = error;
            const isCancelled = err.message.includes('cancelled') || err.message.includes('aborted');
            const isTimedOut = err.message.includes('timeout');
            return {
                taskId: `${sessionId}_${Date.now()}`,
                success: false,
                error: err,
                executionTimeMs: Date.now() - startTime,
                cancelled: isCancelled,
                timedOut: isTimedOut,
            };
        }
    }
    /**
     * Execute in global lane (cross-session)
     */
    async executeGlobal(name, fn, options = {}) {
        return this.execute('__global__', name, fn, { ...options, useGlobalLane: true });
    }
    /**
     * Wait for a condition with timeout (like agent.wait in OpenClaw)
     */
    async wait(sessionId, condition, options = {}) {
        const timeout = options.timeoutMs ?? this.config.defaultWaitTimeoutMs;
        const pollInterval = options.pollIntervalMs ?? 500;
        const startTime = Date.now();
        return this.execute(sessionId, 'wait', async () => {
            while (true) {
                // Check abort
                if (options.abortSignal?.aborted) {
                    throw new Error('Wait was aborted');
                }
                // Check timeout
                if (Date.now() - startTime > timeout) {
                    throw new Error(`Wait timed out after ${timeout}ms`);
                }
                // Check condition
                const result = await condition();
                if (result !== null) {
                    return result;
                }
                // Poll interval
                await new Promise(resolve => setTimeout(resolve, pollInterval));
            }
        }, { timeoutMs: timeout, abortSignal: options.abortSignal }).then(r => {
            if (!r.success)
                throw r.error;
            return r.result;
        });
    }
    /**
     * Get or create a session lane
     */
    getOrCreateLane(sessionId) {
        let lane = this.lanes.get(sessionId);
        if (!lane) {
            lane = new SessionLane(sessionId, this.config);
            this.setupLaneEvents(lane);
            this.lanes.set(sessionId, lane);
            this.emit('lane:created', sessionId);
            logger.debug(`Created session lane: ${sessionId}`);
        }
        return lane;
    }
    /**
     * Setup event forwarding from lane
     */
    setupLaneEvents(lane) {
        lane.on('task:queued', (sessionId, taskId) => this.emit('task:queued', sessionId, taskId));
        lane.on('task:started', (sessionId, taskId) => this.emit('task:started', sessionId, taskId));
        lane.on('task:completed', (sessionId, taskId, duration) => this.emit('task:completed', sessionId, taskId, duration));
        lane.on('task:failed', (sessionId, taskId, error) => this.emit('task:failed', sessionId, taskId, error));
        lane.on('task:timeout', (sessionId, taskId) => this.emit('task:timeout', sessionId, taskId));
        lane.on('task:cancelled', (sessionId, taskId) => this.emit('task:cancelled', sessionId, taskId));
    }
    /**
     * Destroy a session lane
     */
    async destroyLane(sessionId) {
        const lane = this.lanes.get(sessionId);
        if (!lane)
            return false;
        await lane.drain();
        this.lanes.delete(sessionId);
        this.emit('lane:destroyed', sessionId);
        logger.debug(`Destroyed session lane: ${sessionId}`);
        return true;
    }
    /**
     * Cancel all tasks in a session
     */
    cancelSession(sessionId) {
        const lane = this.lanes.get(sessionId);
        if (!lane)
            return 0;
        return lane.cancelAll();
    }
    /**
     * Get lane info
     */
    getLaneInfo(sessionId) {
        const lane = sessionId === '__global__' ? this.globalLane : this.lanes.get(sessionId);
        return lane?.getInfo() ?? null;
    }
    /**
     * Get all lanes info
     */
    getAllLanesInfo() {
        const infos = [this.globalLane.getInfo()];
        for (const lane of this.lanes.values()) {
            infos.push(lane.getInfo());
        }
        return infos;
    }
    /**
     * Start cleanup interval
     */
    startCleanupInterval() {
        if (this.cleanupInterval)
            return;
        this.cleanupInterval = setInterval(() => {
            this.cleanupIdleLanes();
        }, 60_000); // Check every minute
    }
    /**
     * Stop cleanup interval
     */
    stopCleanupInterval() {
        if (this.cleanupInterval) {
            clearInterval(this.cleanupInterval);
            this.cleanupInterval = null;
        }
    }
    /**
     * Cleanup idle lanes
     */
    cleanupIdleLanes() {
        let cleaned = 0;
        for (const [sessionId, lane] of this.lanes) {
            if (lane.isIdleForCleanup(this.config.laneIdleTimeoutMs)) {
                this.lanes.delete(sessionId);
                this.emit('lane:destroyed', sessionId);
                cleaned++;
                logger.debug(`Cleaned up idle lane: ${sessionId}`);
            }
        }
        return cleaned;
    }
    /**
     * Get statistics
     */
    getStats() {
        const infos = this.getAllLanesInfo();
        const sessionLanes = infos.filter(i => i.sessionId !== '__global__');
        return {
            totalLanes: sessionLanes.length,
            activeLanes: sessionLanes.filter(i => i.status !== 'idle').length,
            idleLanes: sessionLanes.filter(i => i.status === 'idle').length,
            totalQueued: sessionLanes.reduce((sum, i) => sum + i.queueSize, 0),
            globalLaneInfo: this.globalLane.getInfo(),
        };
    }
    /**
     * Shutdown the manager
     */
    async shutdown() {
        this.stopCleanupInterval();
        // Drain all lanes
        const drainPromises = [];
        drainPromises.push(this.globalLane.drain());
        for (const lane of this.lanes.values()) {
            drainPromises.push(lane.drain());
        }
        await Promise.all(drainPromises);
        this.lanes.clear();
        logger.debug('Session lanes manager shutdown complete');
    }
}
// ============================================================================
// Singleton
// ============================================================================
let sessionLanesInstance = null;
/**
 * Get session lanes manager instance
 */
export function getSessionLanesManager(config) {
    if (!sessionLanesInstance) {
        sessionLanesInstance = new SessionLanesManager(config);
    }
    return sessionLanesInstance;
}
/**
 * Reset session lanes manager
 */
export async function resetSessionLanesManager() {
    if (sessionLanesInstance) {
        await sessionLanesInstance.shutdown();
        sessionLanesInstance = null;
    }
}
export default SessionLanesManager;
//# sourceMappingURL=session-lanes.js.map