/**
 * Session Manager Service
 *
 * This service manages agent sessions with provider session ID support for
 * resumable sessions and cross-restart persistence.
 *
 * Key features:
 * - Track active sessions with process metadata
 * - Persist session state to database via Agent entity metadata
 * - Start, resume, stop, and suspend sessions
 * - Message running sessions via agent channels
 * - Query session history per agent
 *
 * @module
 */
import { EventEmitter } from 'node:events';
import { createTimestamp } from '@stoneforge/core';
import { getAgentMetadata } from '../api/orchestrator-api.js';
import { getProviderRegistry } from '../providers/registry.js';
import { ClaudeAgentProvider } from '../providers/claude/index.js';
import { OpenCodeAgentProvider } from '../providers/opencode/index.js';
import { CodexAgentProvider } from '../providers/codex/index.js';
import { trackListeners } from './event-utils.js';
// ============================================================================
// Session Manager Implementation
// ============================================================================
/**
 * Implementation of the Session Manager.
 */
export class SessionManagerImpl {
    spawner;
    api;
    registry;
    settingsService;
    sessions = new Map();
    agentSessions = new Map(); // agentId -> active sessionId
    sessionHistory = new Map();
    sessionCleanupFns = new Map(); // sessionId -> cleanup function
    operationLog;
    constructor(spawner, api, // Used for message operations in messageSession
    registry, settingsService) {
        this.spawner = spawner;
        this.api = api;
        this.registry = registry;
        this.settingsService = settingsService;
        // Subscribe to spawner events to track session state
        this.setupSpawnerEventHandlers();
    }
    /**
     * Gets the API instance for direct operations
     */
    getApi() {
        return this.api;
    }
    /**
     * Sets the operation log service for persistent event logging.
     * Called after construction to avoid circular dependency issues.
     */
    setOperationLog(log) {
        this.operationLog = log;
    }
    // ----------------------------------------
    // Session Lifecycle
    // ----------------------------------------
    async startSession(agentId, options) {
        // Get agent to determine role and mode
        const agent = await this.registry.getAgent(agentId);
        if (!agent) {
            throw new Error(`Agent not found: ${agentId}`);
        }
        const meta = getAgentMetadata(agent);
        if (!meta) {
            throw new Error(`Entity is not a valid agent: ${agentId}`);
        }
        // Check if agent already has an active session
        const existingSessionId = this.agentSessions.get(agentId);
        if (existingSessionId) {
            const existingSession = this.sessions.get(existingSessionId);
            if (existingSession && existingSession.status === 'running') {
                throw new Error(`Agent ${agentId} already has an active session: ${existingSessionId}`);
            }
        }
        // Determine interactive mode based on agent role if not explicitly specified
        // Directors and persistent workers use interactive mode (PTY)
        // Ephemeral workers and stewards use headless mode (stream-json)
        const isInteractiveByRole = meta.agentRole === 'director' ||
            (meta.agentRole === 'worker' && meta.workerMode === 'persistent');
        const useInteractive = options?.interactive ?? isInteractiveByRole;
        // Resolve provider from agent metadata, with executable path resolution.
        // executablePathOverride (from rate limit fallback) takes highest priority.
        const providerName = meta.provider;
        const agentExecutablePath = options?.executablePathOverride ?? meta.executablePath;
        const providerOverride = await this.resolveProvider(providerName, agentExecutablePath);
        // Resolve model: use options override, or fall back to agent metadata
        const modelOverride = options?.model ?? meta.model;
        // Build spawn options.
        // Pass the resolved executable path as claudePath so the spawner can track
        // the actual executable used for rate-limit event attribution.
        const resolvedExecutablePath = this.resolveExecutablePath(providerName ?? 'claude-code', agentExecutablePath);
        const spawnOptions = {
            workingDirectory: options?.workingDirectory,
            initialPrompt: options?.initialPrompt,
            environmentVariables: options?.environmentVariables,
            mode: useInteractive ? 'interactive' : 'headless',
            cols: options?.cols,
            rows: options?.rows,
            provider: providerOverride,
            model: modelOverride,
            claudePath: resolvedExecutablePath,
        };
        console.log('[session-manager] Starting session for agent', agentId, 'mode:', spawnOptions.mode, 'provider:', providerName ?? 'claude-code', 'model:', modelOverride ?? 'default', 'executablePath:', resolvedExecutablePath ?? 'default', 'prompt length:', options?.initialPrompt?.length ?? 0);
        // Spawn the session
        const result = await this.spawner.spawn(agentId, meta.agentRole, spawnOptions);
        // Create internal session state
        const sessionState = this.createSessionState(result, agentId, meta, options);
        // Track the session
        this.sessions.set(sessionState.id, sessionState);
        this.agentSessions.set(agentId, sessionState.id);
        // Forward events from spawner BEFORE any awaits to avoid missing
        // early exit events (e.g. when provider uses `exec` and the process
        // terminates before the awaits below complete).
        this.setupSessionEventForwarding(sessionState, result.events);
        // Update agent's session status in database
        await this.registry.updateAgentSession(agentId, result.session.providerSessionId, 'running');
        // Persist session state
        await this.persistSession(sessionState.id);
        // Log session spawn
        this.operationLog?.write('info', 'session', `Session started for agent ${agentId} (${meta.agentRole})`, { agentId, sessionId: sessionState.id });
        return {
            session: this.toPublicSession(sessionState),
            events: sessionState.events,
        };
    }
    async resumeSession(agentId, options) {
        // Get agent to determine role and mode
        const agent = await this.registry.getAgent(agentId);
        if (!agent) {
            throw new Error(`Agent not found: ${agentId}`);
        }
        const meta = getAgentMetadata(agent);
        if (!meta) {
            throw new Error(`Entity is not a valid agent: ${agentId}`);
        }
        // Check if agent already has an active session
        const existingSessionId = this.agentSessions.get(agentId);
        if (existingSessionId) {
            const existingSession = this.sessions.get(existingSessionId);
            if (existingSession && existingSession.status === 'running') {
                throw new Error(`Agent ${agentId} already has an active session: ${existingSessionId}`);
            }
        }
        // Perform UWP check before resuming (if enabled)
        // This implements the Universal Work Principle: check queue before continuing previous context
        let uwpCheck;
        const shouldCheckQueue = options.checkReadyQueue !== false; // Default to true
        if (shouldCheckQueue && options.getReadyTasks) {
            const readyTasks = await options.getReadyTasks(agentId, 1);
            if (readyTasks.length > 0) {
                const task = readyTasks[0];
                uwpCheck = {
                    hasReadyTask: true,
                    taskId: task.id,
                    taskTitle: task.title,
                    taskPriority: task.priority,
                    shouldProcessFirst: true,
                };
            }
            else {
                uwpCheck = {
                    hasReadyTask: false,
                    shouldProcessFirst: false,
                };
            }
        }
        // Build the resume prompt, prepending task instructions if UWP found a task
        let effectivePrompt = options.resumePrompt;
        if (uwpCheck?.hasReadyTask && uwpCheck.shouldProcessFirst) {
            const taskInstructions = this.buildUWPTaskPrompt(uwpCheck);
            effectivePrompt = effectivePrompt
                ? `${taskInstructions}\n\n${effectivePrompt}`
                : taskInstructions;
        }
        // Look up the original session's working directory from history if not provided
        let workingDirectory = options.workingDirectory;
        if (!workingDirectory) {
            const history = await this.getSessionHistory(agentId, 20);
            const previousSession = history.find(h => h.providerSessionId === options.providerSessionId);
            if (previousSession?.workingDirectory) {
                workingDirectory = previousSession.workingDirectory;
            }
        }
        // Resolve provider from agent metadata, with executable path resolution.
        // Pass the resolved executable path as claudePath so the spawner can track
        // the actual executable used for rate-limit event attribution.
        const providerName = meta.provider;
        const agentExecutablePath = meta.executablePath;
        const providerOverride = await this.resolveProvider(providerName, agentExecutablePath);
        const resolvedExecutablePath = this.resolveExecutablePath(providerName ?? 'claude-code', agentExecutablePath);
        // Resolve model from agent metadata (resume doesn't allow model override)
        const modelFromMeta = meta.model;
        // Build spawn options with resume
        const spawnOptions = {
            workingDirectory,
            resumeSessionId: options.providerSessionId,
            initialPrompt: effectivePrompt,
            provider: providerOverride,
            model: modelFromMeta,
            claudePath: resolvedExecutablePath,
        };
        // Spawn the session with resume
        const result = await this.spawner.spawn(agentId, meta.agentRole, spawnOptions);
        // Create internal session state
        const sessionState = this.createSessionState(result, agentId, meta, {
            workingDirectory: options.workingDirectory,
            worktree: options.worktree,
        });
        // Track the session
        this.sessions.set(sessionState.id, sessionState);
        this.agentSessions.set(agentId, sessionState.id);
        // Forward events from spawner BEFORE any awaits to avoid missing
        // early exit events (e.g. when provider uses `exec` and the process
        // terminates before the awaits below complete).
        this.setupSessionEventForwarding(sessionState, result.events);
        // Update agent's session status in database
        await this.registry.updateAgentSession(agentId, options.providerSessionId, 'running');
        // Persist session state
        await this.persistSession(sessionState.id);
        return {
            session: this.toPublicSession(sessionState),
            events: sessionState.events,
            uwpCheck,
        };
    }
    async stopSession(sessionId, options) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new Error(`Session not found: ${sessionId}`);
        }
        // Clean up event listeners to prevent leaks
        this.cleanupSessionEventListeners(sessionId);
        // Update session state BEFORE terminating to prevent race with exit event handler
        const updatedSession = {
            ...session,
            status: 'terminated',
            endedAt: createTimestamp(),
            terminationReason: options?.reason,
            persisted: false,
        };
        this.sessions.set(sessionId, updatedSession);
        // Clear active session for agent
        if (this.agentSessions.get(session.agentId) === sessionId) {
            this.agentSessions.delete(session.agentId);
        }
        // Add to history (do this before terminate to avoid race with exit handler)
        this.addToHistory(session.agentId, updatedSession);
        // Now terminate via spawner (may trigger exit event, but status already terminated)
        await this.spawner.terminate(sessionId, options?.graceful ?? true);
        // Update agent's session status in database
        await this.registry.updateAgentSession(session.agentId, undefined, 'idle');
        // Persist session state
        await this.persistSession(sessionId);
        // Schedule cleanup of terminated session from memory (M-1)
        this.scheduleTerminatedSessionCleanup(sessionId);
        // Emit status change
        session.events.emit('status', 'terminated');
        // Log session termination
        this.operationLog?.write('info', 'session', `Session terminated for agent ${session.agentId}${options?.reason ? `: ${options.reason}` : ''}`, { agentId: session.agentId, sessionId });
    }
    async interruptSession(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new Error(`Session not found: ${sessionId}`);
        }
        if (session.status !== 'running') {
            throw new Error(`Cannot interrupt session in status: ${session.status}`);
        }
        // Interrupt via spawner
        await this.spawner.interrupt(sessionId);
        // Emit interrupt event
        session.events.emit('interrupt');
    }
    async suspendSession(sessionId, reason) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            throw new Error(`Session not found: ${sessionId}`);
        }
        // Clean up event listeners to prevent leaks
        this.cleanupSessionEventListeners(sessionId);
        // Update session state BEFORE suspending to prevent race with exit event handler
        const updatedSession = {
            ...session,
            status: 'suspended',
            endedAt: createTimestamp(),
            terminationReason: reason,
            persisted: false,
        };
        this.sessions.set(sessionId, updatedSession);
        // Clear active session for agent (but keep in sessions map for resume)
        if (this.agentSessions.get(session.agentId) === sessionId) {
            this.agentSessions.delete(session.agentId);
        }
        // Now suspend via spawner (may trigger exit event, but status already 'suspended')
        try {
            await this.spawner.suspend(sessionId);
        }
        catch (error) {
            // Revert status on failure
            this.sessions.set(sessionId, session);
            if (this.agentSessions.get(session.agentId) !== sessionId) {
                this.agentSessions.set(session.agentId, sessionId);
            }
            throw error;
        }
        // Add to history
        this.addToHistory(session.agentId, updatedSession);
        // Update agent's session status in database
        await this.registry.updateAgentSession(session.agentId, session.providerSessionId, 'suspended');
        // Persist session state
        await this.persistSession(sessionId);
        // Emit status change
        updatedSession.events.emit('status', 'suspended');
    }
    // ----------------------------------------
    // Session Queries
    // ----------------------------------------
    getSession(sessionId) {
        const session = this.sessions.get(sessionId);
        return session ? this.toPublicSession(session) : undefined;
    }
    getActiveSession(agentId) {
        const sessionId = this.agentSessions.get(agentId);
        if (!sessionId) {
            return undefined;
        }
        const session = this.sessions.get(sessionId);
        // Allow both 'starting' and 'running' status - 'starting' is needed for SSE
        // connections that happen before the Claude CLI emits its init event
        if (!session || (session.status !== 'running' && session.status !== 'starting')) {
            return undefined;
        }
        // Validate that the process/session is actually alive
        if (session.pid) {
            // Interactive sessions: check OS process liveness
            if (!this.isProcessAlive(session.pid)) {
                this.cleanupDeadSession(session);
                return undefined;
            }
        }
        else {
            // Headless sessions (no PID): cross-reference with spawner
            const spawnerSession = this.spawner.getSession(sessionId);
            if (!spawnerSession || spawnerSession.status === 'terminated') {
                this.cleanupDeadSession(session);
                return undefined;
            }
        }
        return this.toPublicSession(session);
    }
    listSessions(filter) {
        let sessions = Array.from(this.sessions.values());
        // Validate liveness for sessions in active states.
        // This catches sessions whose processes exited without the exit handler firing.
        const activeStatuses = ['starting', 'running', 'terminating'];
        let didCleanup = false;
        for (const session of sessions) {
            if (!activeStatuses.includes(session.status))
                continue;
            if (session.pid) {
                // Interactive sessions: check if the OS process is still alive
                if (!this.isProcessAlive(session.pid)) {
                    this.cleanupDeadSession(session);
                    didCleanup = true;
                }
            }
            else {
                // Headless sessions (no PID): cross-reference with the spawner.
                // If the spawner no longer tracks this session, the process has exited
                // and the exit event was lost or already processed by the spawner.
                const spawnerSession = this.spawner.getSession(session.id);
                if (!spawnerSession || spawnerSession.status === 'terminated') {
                    this.cleanupDeadSession(session);
                    didCleanup = true;
                }
            }
        }
        // Re-read after potential cleanups
        if (didCleanup) {
            sessions = Array.from(this.sessions.values());
        }
        if (filter) {
            if (filter.agentId !== undefined) {
                sessions = sessions.filter((s) => s.agentId === filter.agentId);
            }
            if (filter.role !== undefined) {
                sessions = sessions.filter((s) => s.agentRole === filter.role);
            }
            if (filter.status !== undefined) {
                const statuses = Array.isArray(filter.status) ? filter.status : [filter.status];
                sessions = sessions.filter((s) => statuses.includes(s.status));
            }
            if (filter.startedAfter !== undefined) {
                const afterTime = this.getTimestampMs(filter.startedAfter);
                sessions = sessions.filter((s) => {
                    if (!s.startedAt)
                        return false;
                    return this.getTimestampMs(s.startedAt) >= afterTime;
                });
            }
            if (filter.startedBefore !== undefined) {
                const beforeTime = this.getTimestampMs(filter.startedBefore);
                sessions = sessions.filter((s) => {
                    if (!s.startedAt)
                        return false;
                    return this.getTimestampMs(s.startedAt) <= beforeTime;
                });
            }
            if (filter.resumable !== undefined) {
                sessions = sessions.filter((s) => filter.resumable ? s.providerSessionId !== undefined : s.providerSessionId === undefined);
            }
        }
        return sessions.map((s) => this.toPublicSession(s));
    }
    getMostRecentResumableSession(agentId) {
        const agentSessions = Array.from(this.sessions.values())
            .filter((s) => s.agentId === agentId && s.providerSessionId !== undefined)
            .sort((a, b) => {
            const aTime = this.getTimestampMs(a.createdAt);
            const bTime = this.getTimestampMs(b.createdAt);
            return bTime - aTime;
        });
        return agentSessions.length > 0 ? this.toPublicSession(agentSessions[0]) : undefined;
    }
    async getSessionHistory(agentId, limit = 10) {
        // First, check in-memory history
        const inMemoryHistory = this.sessionHistory.get(agentId) ?? [];
        // Load from agent metadata if needed
        const agent = await this.registry.getAgent(agentId);
        if (!agent) {
            return inMemoryHistory.slice(0, limit);
        }
        const meta = getAgentMetadata(agent);
        if (!meta) {
            return inMemoryHistory.slice(0, limit);
        }
        // Get persisted history from agent metadata
        const persistedHistory = this.getPersistedHistory(agent);
        // Merge and dedupe
        const allHistory = this.mergeHistory(inMemoryHistory, persistedHistory);
        return allHistory.slice(0, limit);
    }
    async getSessionHistoryByRole(role, limit = 10) {
        // Get all agents with the specified role
        const agents = await this.registry.getAgentsByRole(role);
        // Collect all session history entries from all agents with this role
        const allRoleHistory = [];
        for (const agent of agents) {
            // Agent.id is ElementId but we need EntityId for session history
            const agentId = agent.id;
            const agentHistory = await this.getSessionHistory(agentId, limit);
            const agentMeta = getAgentMetadata(agent);
            // Convert to role-based entries
            for (const entry of agentHistory) {
                allRoleHistory.push({
                    ...entry,
                    role: agentMeta?.agentRole ?? role,
                    agentId: agentId,
                    agentName: agent.name,
                });
            }
        }
        // Sort by ended or started time, most recent first
        allRoleHistory.sort((a, b) => {
            const aTime = a.endedAt
                ? this.getTimestampMs(a.endedAt)
                : a.startedAt
                    ? this.getTimestampMs(a.startedAt)
                    : 0;
            const bTime = b.endedAt
                ? this.getTimestampMs(b.endedAt)
                : b.startedAt
                    ? this.getTimestampMs(b.startedAt)
                    : 0;
            return bTime - aTime;
        });
        return allRoleHistory.slice(0, limit);
    }
    async getPreviousSession(role) {
        // Get session history for the role
        const roleHistory = await this.getSessionHistoryByRole(role, 100);
        // Find the most recent session that has ended (suspended or terminated)
        const previousSession = roleHistory.find((entry) => entry.status === 'suspended' || entry.status === 'terminated');
        return previousSession;
    }
    // ----------------------------------------
    // Session Communication
    // ----------------------------------------
    async messageSession(sessionId, options) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            return { success: false, error: `Session not found: ${sessionId}` };
        }
        // Validate options
        if (!options.contentRef && !options.content) {
            return { success: false, error: 'Either contentRef or content must be provided' };
        }
        // Get agent's channel
        const agent = await this.registry.getAgent(session.agentId);
        if (!agent) {
            return { success: false, error: `Agent not found: ${session.agentId}` };
        }
        const meta = getAgentMetadata(agent);
        const channelId = meta?.channelId;
        if (!channelId) {
            return { success: false, error: `Agent has no channel: ${session.agentId}` };
        }
        try {
            // Resolve message content
            let messageContent = options.content;
            if (!messageContent && options.contentRef) {
                const doc = await this.api.get(options.contentRef);
                if (doc && 'content' in doc) {
                    messageContent = doc.content;
                }
            }
            if (!messageContent) {
                return { success: false, error: 'Could not resolve message content' };
            }
            // Format the message with sender context
            const senderId = options.senderId ?? 'system';
            const formattedMessage = `[Message from ${senderId}]: ${messageContent}`;
            // Forward to the running process via spawner
            if (session.mode === 'interactive') {
                // Write the message content first
                await this.spawner.writeToPty(sessionId, formattedMessage);
                // Wait for the terminal to process the pasted content before sending Enter.
                // The carriage return must be sent as a separate message to ensure proper submission.
                // A longer delay (1500ms) is needed to ensure large messages are fully received
                // by the PTY before the Enter key is sent.
                await new Promise((resolve) => setTimeout(resolve, 1500));
                await this.spawner.writeToPty(sessionId, '\r');
            }
            else {
                await this.spawner.sendInput(sessionId, formattedMessage);
            }
            // Update session activity
            const updatedSession = {
                ...session,
                lastActivityAt: createTimestamp(),
                persisted: false,
            };
            this.sessions.set(sessionId, updatedSession);
            return { success: true };
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            return { success: false, error: errorMessage };
        }
    }
    getEventEmitter(sessionId) {
        const session = this.sessions.get(sessionId);
        return session?.events;
    }
    // ----------------------------------------
    // User Idle Tracking
    // ----------------------------------------
    recordUserInput(sessionId) {
        const session = this.sessions.get(sessionId);
        if (session) {
            session.lastUserInputAt = Date.now();
        }
    }
    getSessionUserIdleMs(agentId) {
        const sessionId = this.agentSessions.get(agentId);
        if (!sessionId)
            return undefined;
        const session = this.sessions.get(sessionId);
        if (!session || !session.lastUserInputAt)
            return undefined;
        return Date.now() - session.lastUserInputAt;
    }
    // ----------------------------------------
    // Persistence
    // ----------------------------------------
    async persistSession(sessionId) {
        const session = this.sessions.get(sessionId);
        if (!session) {
            return;
        }
        // Get current agent state
        const agent = await this.registry.getAgent(session.agentId);
        if (!agent) {
            return;
        }
        // Get the current in-memory session history for this agent
        const inMemoryHistory = this.sessionHistory.get(session.agentId) ?? [];
        // Update agent metadata with session info and history
        // This persists session history to database for cross-restart recovery
        await this.registry.updateAgentMetadata(session.agentId, {
            sessionId: session.providerSessionId,
            sessionStatus: session.status === 'running' ? 'running' : session.status === 'suspended' ? 'suspended' : 'idle',
            lastActivityAt: session.lastActivityAt,
            // Persist session history (limited to 20 entries to avoid bloat)
            sessionHistory: inMemoryHistory.slice(0, 20),
        });
        // Mark as persisted
        const updatedSession = {
            ...session,
            persisted: true,
        };
        this.sessions.set(sessionId, updatedSession);
    }
    async loadSessionState(agentId) {
        const agent = await this.registry.getAgent(agentId);
        if (!agent) {
            return;
        }
        const meta = getAgentMetadata(agent);
        if (!meta) {
            return;
        }
        // Load history from agent metadata
        const persistedHistory = this.getPersistedHistory(agent);
        if (persistedHistory.length > 0) {
            this.sessionHistory.set(agentId, persistedHistory);
        }
        // Check if there's a suspended session that can be resumed
        if (meta.sessionId && meta.sessionStatus === 'suspended') {
            // Create a placeholder session record for the suspended session
            const suspendedSession = persistedHistory.find((h) => h.providerSessionId === meta.sessionId && h.status === 'suspended');
            if (suspendedSession) {
                // Determine mode based on agent role - directors and persistent workers use interactive
                const isInteractive = meta.agentRole === 'director' ||
                    (meta.agentRole === 'worker' && meta.workerMode === 'persistent');
                const sessionState = {
                    id: suspendedSession.id,
                    providerSessionId: suspendedSession.providerSessionId,
                    agentId,
                    agentRole: meta.agentRole,
                    workerMode: meta.agentRole === 'worker' ? meta.workerMode : undefined,
                    mode: isInteractive ? 'interactive' : 'headless',
                    status: 'suspended',
                    workingDirectory: suspendedSession.workingDirectory,
                    worktree: suspendedSession.worktree,
                    createdAt: suspendedSession.startedAt ?? createTimestamp(),
                    startedAt: suspendedSession.startedAt,
                    lastActivityAt: suspendedSession.endedAt ?? createTimestamp(),
                    endedAt: suspendedSession.endedAt,
                    terminationReason: suspendedSession.terminationReason,
                    events: new EventEmitter(),
                    persisted: true,
                };
                this.sessions.set(sessionState.id, sessionState);
            }
        }
    }
    async reconcileOnStartup() {
        let reconciled = 0;
        const errors = [];
        try {
            // Find all agents that are marked as 'running' in the database
            const agents = await this.registry.listAgents({ sessionStatus: 'running' });
            for (const agent of agents) {
                const agentId = agent.id;
                const meta = getAgentMetadata(agent);
                if (!meta)
                    continue;
                // Check if there's a live in-memory session for this agent
                const activeSessionId = this.agentSessions.get(agentId);
                if (activeSessionId) {
                    const activeSession = this.sessions.get(activeSessionId);
                    if (activeSession && activeSession.status === 'running') {
                        // Session exists in memory and is running — check PID
                        if (activeSession.pid && this.isProcessAlive(activeSession.pid)) {
                            continue; // Process is alive, nothing to reconcile
                        }
                    }
                }
                // No live session — this agent is stale, reset to idle
                try {
                    await this.registry.updateAgentSession(agentId, undefined, 'idle');
                    reconciled++;
                }
                catch (error) {
                    const msg = `Failed to reset agent ${agent.name} (${agentId}): ${error instanceof Error ? error.message : String(error)}`;
                    errors.push(msg);
                }
            }
        }
        catch (error) {
            errors.push(`Failed to list agents: ${error instanceof Error ? error.message : String(error)}`);
        }
        return { reconciled, errors };
    }
    // ----------------------------------------
    // Private Helpers
    // ----------------------------------------
    /**
     * Resolves the executable path for a provider using the priority chain:
     * 1. Agent-specific executablePath from metadata
     * 2. Workspace-wide defaultExecutablePaths[providerName] from settings service
     * 3. undefined (provider will use its built-in default)
     */
    resolveExecutablePath(providerName, agentExecutablePath) {
        // Priority 1: Agent-specific override
        if (agentExecutablePath) {
            return agentExecutablePath;
        }
        // Priority 2: Workspace-wide default from settings
        if (this.settingsService) {
            const defaults = this.settingsService.getAgentDefaults();
            const workspacePath = defaults.defaultExecutablePaths[providerName];
            if (workspacePath) {
                return workspacePath;
            }
        }
        // Priority 3: No custom path — provider will use its built-in default
        return undefined;
    }
    /**
     * Creates a fresh provider instance with a custom executable path.
     * Returns undefined if no custom path is needed (use registry singleton instead).
     */
    createProviderWithPath(providerName, executablePath) {
        switch (providerName) {
            case 'claude-code':
            case 'claude': // backward compatibility
                return new ClaudeAgentProvider(executablePath);
            case 'opencode':
                return new OpenCodeAgentProvider({ executablePath });
            case 'codex':
                return new CodexAgentProvider({ executablePath });
            default:
                throw new Error(`Unknown provider '${providerName}' — cannot create with custom executable path`);
        }
    }
    /**
     * Resolves the provider for a session, creating a custom instance if a non-default
     * executable path is configured (agent-specific or workspace-wide).
     * Falls back to the registry singleton when no custom path is needed.
     */
    async resolveProvider(providerName, agentExecutablePath) {
        const effectiveProvider = providerName ?? 'claude-code';
        const resolvedPath = this.resolveExecutablePath(effectiveProvider, agentExecutablePath);
        if (resolvedPath) {
            // Custom path configured — create a fresh provider instance with it
            return this.createProviderWithPath(effectiveProvider, resolvedPath);
        }
        // No custom path — use registry singleton for non-default providers
        if (effectiveProvider !== 'claude-code') {
            const registry = getProviderRegistry();
            return registry.getOrThrow(effectiveProvider);
        }
        // Default claude-code provider — no override needed (spawner uses claude by default)
        return undefined;
    }
    scheduleTerminatedSessionCleanup(sessionId) {
        setTimeout(() => {
            const session = this.sessions.get(sessionId);
            if (session && session.status === 'terminated') {
                this.sessions.delete(sessionId);
            }
        }, 5000);
    }
    /**
     * Checks whether a process with the given PID is still alive.
     * Uses signal 0 which doesn't kill — just checks existence.
     */
    isProcessAlive(pid) {
        try {
            process.kill(pid, 0);
            return true;
        }
        catch {
            return false;
        }
    }
    /**
     * Cleans up a session whose process is no longer alive.
     * Transitions it to 'terminated' and schedules memory cleanup.
     */
    cleanupDeadSession(session) {
        const updated = {
            ...session,
            status: 'terminated',
            endedAt: createTimestamp(),
            terminationReason: 'Process no longer alive (PID check)',
            persisted: false,
        };
        this.sessions.set(session.id, updated);
        if (this.agentSessions.get(session.agentId) === session.id) {
            this.agentSessions.delete(session.agentId);
        }
        this.addToHistory(session.agentId, updated);
        this.registry.updateAgentSession(session.agentId, undefined, 'idle').catch(() => { });
        this.persistSession(session.id).then(() => {
            this.scheduleTerminatedSessionCleanup(session.id);
        }).catch(() => { });
    }
    setupSpawnerEventHandlers() {
        // Note: The spawner emits events per-session via the session's event emitter
        // We don't need global event handlers here
    }
    setupSessionEventForwarding(session, spawnerEvents) {
        // Create named handler functions so we can remove them later
        const onEvent = (event) => {
            const current = this.sessions.get(session.id);
            if (!current)
                return;
            current.events.emit('event', event);
            // Update last activity
            this.sessions.set(session.id, {
                ...current,
                lastActivityAt: createTimestamp(),
                persisted: false,
            });
        };
        const onPtyData = (data) => {
            const current = this.sessions.get(session.id);
            if (!current)
                return;
            current.events.emit('pty-data', data);
            // Update last activity
            this.sessions.set(session.id, {
                ...current,
                lastActivityAt: createTimestamp(),
                persisted: false,
            });
        };
        const onError = (error) => {
            const current = this.sessions.get(session.id);
            if (!current)
                return;
            current.events.emit('error', error);
        };
        const onStderr = (data) => {
            const current = this.sessions.get(session.id);
            if (!current)
                return;
            current.events.emit('stderr', data);
        };
        const onRaw = (data) => {
            const current = this.sessions.get(session.id);
            if (!current)
                return;
            current.events.emit('raw', data);
        };
        const onRateLimited = (data) => {
            const current = this.sessions.get(session.id);
            if (!current)
                return;
            current.events.emit('rate_limited', data);
        };
        const onExit = async (code, signal) => {
            console.log(`[session-manager] Exit event received for session ${session.id}, agent ${session.agentId}, code=${code}, signal=${signal}`);
            // Clean up all spawner event listeners immediately to prevent leaks
            this.cleanupSessionEventListeners(session.id, spawnerEvents);
            const exitingSession = this.sessions.get(session.id);
            if (exitingSession) {
                exitingSession.events.emit('exit', code, signal);
            }
            // Update session status if not already updated
            const currentSession = this.sessions.get(session.id);
            if (currentSession && currentSession.status !== 'terminated' && currentSession.status !== 'suspended') {
                console.log(`[session-manager] Cleaning up ${currentSession.status} session ${session.id} for agent ${session.agentId}`);
                // Log unexpected session exits (non-zero exit code or signal)
                if (code !== null && code !== 0) {
                    this.operationLog?.write('error', 'session', `Session ${session.id} exited with code ${code}`, { agentId: session.agentId, sessionId: session.id, exitCode: code });
                }
                else if (signal) {
                    this.operationLog?.write('warn', 'session', `Session ${session.id} killed by signal ${signal}`, { agentId: session.agentId, sessionId: session.id, signal });
                }
                const updatedSession = {
                    ...currentSession,
                    status: 'terminated',
                    endedAt: createTimestamp(),
                    persisted: false,
                };
                this.sessions.set(session.id, updatedSession);
                // Clear active session mapping
                if (this.agentSessions.get(session.agentId) === session.id) {
                    this.agentSessions.delete(session.agentId);
                    console.log(`[session-manager] Cleared active session mapping for agent ${session.agentId}`);
                }
                // Add to history
                this.addToHistory(session.agentId, updatedSession);
                // Update agent status in registry to 'idle'
                try {
                    await this.registry.updateAgentSession(session.agentId, undefined, 'idle');
                    console.log(`[session-manager] Updated agent ${session.agentId} status to idle in registry`);
                }
                catch (error) {
                    console.error(`[session-manager] Failed to update agent ${session.agentId} status:`, error);
                }
                // Persist session state
                try {
                    await this.persistSession(session.id);
                }
                catch (error) {
                    console.error(`[session-manager] Failed to persist session ${session.id}:`, error);
                }
                // Schedule cleanup of terminated session from memory (M-1)
                this.scheduleTerminatedSessionCleanup(session.id);
                updatedSession.events.emit('status', 'terminated');
            }
            else {
                console.log(`[session-manager] Session ${session.id} already in status: ${currentSession?.status ?? 'not found'}`);
            }
        };
        // Update providerSessionId when discovered by the spawner
        const onProviderSessionId = (providerSessionId) => {
            const current = this.sessions.get(session.id);
            if (!current || current.providerSessionId)
                return;
            this.sessions.set(session.id, {
                ...current,
                providerSessionId,
                persisted: false,
            });
            // Persist session so providerSessionId survives a server restart
            this.registry.updateAgentSession(session.agentId, providerSessionId, 'running').catch((err) => {
                console.error(`[session-manager] Failed to persist providerSessionId for ${session.id}:`, err);
            });
            this.persistSession(session.id).catch((err) => {
                console.error(`[session-manager] Failed to persist session after providerSessionId for ${session.id}:`, err);
            });
        };
        // Attach all event listeners with tracked maxListeners
        const cleanup = trackListeners(spawnerEvents, {
            'event': onEvent,
            'pty-data': onPtyData,
            'error': onError,
            'stderr': onStderr,
            'raw': onRaw,
            'rate_limited': onRateLimited,
            'exit': onExit,
            'provider-session-id': onProviderSessionId,
        });
        this.sessionCleanupFns.set(session.id, cleanup);
    }
    /**
     * Cleans up event listeners for a session.
     * Called when a session exits to prevent listener leaks.
     */
    cleanupSessionEventListeners(sessionId, _spawnerEvents) {
        const cleanup = this.sessionCleanupFns.get(sessionId);
        if (cleanup) {
            cleanup();
            this.sessionCleanupFns.delete(sessionId);
            console.log(`[session-manager] Cleaned up event listeners for session ${sessionId}`);
        }
    }
    createSessionState(result, agentId, meta, options) {
        return {
            id: result.session.id,
            providerSessionId: result.session.providerSessionId,
            agentId,
            agentRole: meta.agentRole,
            workerMode: meta.agentRole === 'worker' ? meta.workerMode : undefined,
            mode: result.session.mode,
            pid: result.session.pid,
            status: result.session.status,
            workingDirectory: result.session.workingDirectory,
            worktree: options?.worktree,
            createdAt: result.session.createdAt,
            startedAt: result.session.startedAt,
            lastActivityAt: result.session.lastActivityAt,
            events: new EventEmitter(),
            persisted: false,
        };
    }
    toPublicSession(session) {
        return {
            id: session.id,
            providerSessionId: session.providerSessionId,
            agentId: session.agentId,
            agentRole: session.agentRole,
            workerMode: session.workerMode,
            mode: session.mode,
            pid: session.pid,
            status: session.status,
            workingDirectory: session.workingDirectory,
            worktree: session.worktree,
            createdAt: session.createdAt,
            startedAt: session.startedAt,
            lastActivityAt: session.lastActivityAt,
            endedAt: session.endedAt,
            terminationReason: session.terminationReason,
        };
    }
    addToHistory(agentId, session) {
        const historyEntry = {
            id: session.id,
            providerSessionId: session.providerSessionId,
            status: session.status,
            workingDirectory: session.workingDirectory,
            worktree: session.worktree,
            startedAt: session.startedAt,
            endedAt: session.endedAt,
            terminationReason: session.terminationReason,
        };
        const history = this.sessionHistory.get(agentId) ?? [];
        this.sessionHistory.set(agentId, [historyEntry, ...history.filter((h) => h.id !== session.id)].slice(0, 20));
    }
    getPersistedHistory(agent) {
        // Session history is stored under metadata.agent.sessionHistory
        const agentMeta = agent.metadata?.agent;
        const sessionHistory = agentMeta?.sessionHistory;
        if (!Array.isArray(sessionHistory)) {
            return [];
        }
        // Validate and type-cast
        return sessionHistory.filter((entry) => {
            if (typeof entry !== 'object' || entry === null)
                return false;
            const e = entry;
            return (typeof e.id === 'string' &&
                typeof e.status === 'string' &&
                typeof e.workingDirectory === 'string');
        });
    }
    mergeHistory(inMemory, persisted) {
        const seen = new Set();
        const merged = [];
        // In-memory history takes precedence (more recent)
        for (const entry of inMemory) {
            if (!seen.has(entry.id)) {
                seen.add(entry.id);
                merged.push(entry);
            }
        }
        // Add persisted entries not in memory
        for (const entry of persisted) {
            if (!seen.has(entry.id)) {
                seen.add(entry.id);
                merged.push(entry);
            }
        }
        // Sort by start time descending
        return merged.sort((a, b) => {
            const aTime = a.startedAt ? this.getTimestampMs(a.startedAt) : 0;
            const bTime = b.startedAt ? this.getTimestampMs(b.startedAt) : 0;
            return bTime - aTime;
        });
    }
    getTimestampMs(timestamp) {
        return typeof timestamp === 'number' ? timestamp : new Date(timestamp).getTime();
    }
    /**
     * Builds the UWP task prompt to instruct the agent to process the assigned task first.
     * This ensures compliance with the Universal Work Principle: "If there is work on your anchor, YOU MUST RUN IT"
     */
    buildUWPTaskPrompt(uwpCheck) {
        const parts = [
            '**IMPORTANT: Task Assigned During Suspension**',
            '',
            'Before continuing with any previous context, you must first process the following assigned task:',
            '',
            `- Task ID: ${uwpCheck.taskId}`,
        ];
        if (uwpCheck.taskTitle) {
            parts.push(`- Title: ${uwpCheck.taskTitle}`);
        }
        if (uwpCheck.taskPriority !== undefined) {
            parts.push(`- Priority: ${uwpCheck.taskPriority}`);
        }
        parts.push('', 'Please check the task details and begin working on it immediately.', 'Use `sf task get ' + uwpCheck.taskId + '` to retrieve full task details.');
        return parts.join('\n');
    }
}
// ============================================================================
// Factory Function
// ============================================================================
/**
 * Creates a SessionManager instance
 */
export function createSessionManager(spawner, api, registry, settingsService) {
    return new SessionManagerImpl(spawner, api, registry, settingsService);
}
//# sourceMappingURL=session-manager.js.map