/**
 * Steward Scheduler Service
 *
 * This service executes stewards on schedule (cron) or in response to events.
 * It manages the lifecycle of scheduled steward executions and tracks execution history.
 *
 * Key features:
 * - Cron-based scheduling using node-cron
 * - Event-driven triggers with condition evaluation
 * - Execution history tracking
 * - Graceful shutdown support
 *
 * TB-O23: Steward Scheduler Service
 *
 * @module
 */
import { EventEmitter } from 'node:events';
import { createTimestamp } from '@stoneforge/core';
import { isCronTrigger, isEventTrigger, } from '../types/index.js';
import { loadRolePrompt } from '../prompts/index.js';
import { getAgentMetadata } from './agent-registry.js';
import { createLogger } from '../utils/logger.js';
const logger = createLogger('steward-scheduler');
// ============================================================================
// Cron Schedule Utilities
// ============================================================================
/**
 * Validates a cron expression.
 * This is a basic validation - we check the format but not all edge cases.
 */
export function isValidCronExpression(schedule) {
    // Standard cron format: * * * * * (minute hour day month weekday)
    // Extended format with seconds: * * * * * * (second minute hour day month weekday)
    const parts = schedule.trim().split(/\s+/);
    if (parts.length < 5 || parts.length > 6) {
        return false;
    }
    // Basic validation: each part should be a valid cron field
    const cronFieldPattern = /^(\*|\d+(-\d+)?(,\d+(-\d+)?)*)(\/\d+)?$/;
    return parts.every(part => cronFieldPattern.test(part) || part === '*');
}
/**
 * Calculates the next run time for a cron expression.
 * This is a simplified implementation - for production use node-cron handles this.
 */
export function getNextCronRunTime(_schedule) {
    // This is a placeholder - node-cron provides this via the scheduled task
    // For now we return undefined; actual implementation uses node-cron internals
    return undefined;
}
// ============================================================================
// Condition Evaluation
// ============================================================================
/**
 * Validates that a condition string contains only safe expression tokens.
 * Allows: property access, comparisons, string/number literals, booleans,
 * logical operators, parentheses.
 * Rejects: function calls, assignments, semicolons, template literals,
 * dangerous globals.
 */
function isSafeCondition(condition) {
    // Strip string literals so their contents don't trigger false positives
    const withoutStrings = condition
        .replace(/'[^']*'/g, '""')
        .replace(/"[^"]*"/g, '""');
    // Reject dangerous patterns
    const dangerousPatterns = [
        /[;{}]/, // statements, blocks
        /\b(import|require|eval|Function|constructor|__proto__|prototype)\b/,
        /\b(process|global|globalThis|window|document)\b/,
        /\b(setTimeout|setInterval|fetch|XMLHttpRequest)\b/,
        /=>|\.\.\.|\+\+|--/, // arrow functions, spread, increment/decrement
        /\[.*\]/, // bracket notation (property access escape vector)
        /`/, // template literals
        /\$\{/, // template expressions
        /\bthis\b/, // this reference
        /\bnew\b/, // constructor calls
        /\bdelete\b/, // delete operator
        /\bvoid\b/, // void operator
        /\btypeof\b/, // typeof operator
        /\bin\b/, // in operator
        /\binstanceof\b/, // instanceof operator
        /[^=!<>]=[^=]/, // assignment (but not ==, !=, <=, >=)
    ];
    for (const pattern of dangerousPatterns) {
        if (pattern.test(withoutStrings)) {
            return false;
        }
    }
    // Only allow safe characters after string removal
    const safeTokenPattern = /^[\w\s.$?!&|><=()'",-]+$/;
    if (!safeTokenPattern.test(withoutStrings)) {
        return false;
    }
    // Reject function calls: identifier followed by (
    if (/\w\s*\(/.test(withoutStrings)) {
        return false;
    }
    return true;
}
/**
 * Evaluates a simple condition expression against event data.
 * Supports basic JavaScript-like expressions with property access.
 *
 * Examples:
 * - "task.status === 'closed'"
 * - "task.assignedAgent?.role === 'worker'"
 * - "branch.tests === 'passing'"
 */
export function evaluateCondition(condition, context) {
    try {
        // Validate condition contains only safe expression tokens
        if (!isSafeCondition(condition)) {
            return false;
        }
        const contextKeys = Object.keys(context);
        const contextValues = Object.values(context);
        const fn = new Function(...contextKeys, `"use strict"; try { return Boolean(${condition}); } catch { return false; }`);
        return fn(...contextValues);
    }
    catch {
        return false;
    }
}
// ============================================================================
// Steward Scheduler Implementation
// ============================================================================
/**
 * Default configuration values
 */
const DEFAULT_CONFIG = {
    maxHistoryPerSteward: 100,
    defaultTimeoutMs: 5 * 60 * 1000, // 5 minutes
    startImmediately: false,
};
/**
 * Implementation of the Steward Scheduler Service.
 */
export class StewardSchedulerImpl {
    agentRegistry;
    config;
    executor;
    emitter;
    running = false;
    cronJobs = new Map(); // key: stewardId-triggerIndex
    eventSubscriptions = new Map(); // key: eventName
    executionHistory = [];
    runningExecutions = new Set();
    executionCounter = 0;
    constructor(agentRegistry, executor, config = {}) {
        this.agentRegistry = agentRegistry;
        this.executor = executor;
        this.config = { ...DEFAULT_CONFIG, ...config };
        this.emitter = new EventEmitter();
    }
    // ----------------------------------------
    // Lifecycle
    // ----------------------------------------
    async start() {
        if (this.running) {
            return;
        }
        this.running = true;
        // Start all registered cron jobs
        for (const job of this.cronJobs.values()) {
            this.startCronJob(job);
        }
        // Activate all event subscriptions
        for (const subs of this.eventSubscriptions.values()) {
            for (const sub of subs) {
                sub.isActive = true;
            }
        }
        // Register all stewards if configured to do so
        if (this.config.startImmediately) {
            await this.registerAllStewards();
        }
        logger.info(`Started with ${this.cronJobs.size} cron job(s) and ${[...this.eventSubscriptions.values()].flat().length} event subscription(s)`);
    }
    async stop() {
        if (!this.running) {
            return;
        }
        this.running = false;
        // Stop all cron jobs
        for (const job of this.cronJobs.values()) {
            this.stopCronJob(job);
        }
        // Deactivate event subscriptions
        for (const subs of this.eventSubscriptions.values()) {
            for (const sub of subs) {
                sub.isActive = false;
            }
        }
    }
    isRunning() {
        return this.running;
    }
    // ----------------------------------------
    // Steward Management
    // ----------------------------------------
    async registerSteward(stewardId) {
        const agent = await this.agentRegistry.getAgent(stewardId);
        if (!agent) {
            return false;
        }
        const metadata = getAgentMetadata(agent);
        if (!metadata || metadata.agentRole !== 'steward') {
            return false;
        }
        const stewardMeta = metadata;
        const triggers = stewardMeta.triggers ?? [];
        // Remove existing registrations for this steward
        await this.unregisterSteward(stewardId);
        // Register each trigger
        for (let i = 0; i < triggers.length; i++) {
            const trigger = triggers[i];
            if (isCronTrigger(trigger)) {
                const jobKey = `${stewardId}-${i}`;
                const jobState = {
                    stewardId,
                    stewardName: agent.name,
                    trigger,
                    isRunning: false,
                };
                this.cronJobs.set(jobKey, jobState);
                // Start job if scheduler is running
                if (this.running) {
                    this.startCronJob(jobState);
                }
            }
            else if (isEventTrigger(trigger)) {
                const eventName = trigger.event;
                if (!this.eventSubscriptions.has(eventName)) {
                    this.eventSubscriptions.set(eventName, []);
                }
                this.eventSubscriptions.get(eventName).push({
                    stewardId,
                    stewardName: agent.name,
                    trigger,
                    isActive: this.running,
                });
            }
        }
        const cronCount = triggers.filter(t => isCronTrigger(t)).length;
        const eventCount = triggers.filter(t => isEventTrigger(t)).length;
        logger.info(`Registered steward '${agent.name}' (${stewardId}) with ${cronCount} cron trigger(s) and ${eventCount} event trigger(s)`);
        this.emitter.emit('steward:registered', stewardId);
        return true;
    }
    async unregisterSteward(stewardId) {
        let unregistered = false;
        // Remove cron jobs
        for (const [key, job] of this.cronJobs.entries()) {
            if (job.stewardId === stewardId) {
                this.stopCronJob(job);
                this.cronJobs.delete(key);
                unregistered = true;
            }
        }
        // Remove event subscriptions
        for (const [eventName, subs] of this.eventSubscriptions.entries()) {
            const remaining = subs.filter(s => s.stewardId !== stewardId);
            if (remaining.length !== subs.length) {
                unregistered = true;
            }
            if (remaining.length === 0) {
                this.eventSubscriptions.delete(eventName);
            }
            else {
                this.eventSubscriptions.set(eventName, remaining);
            }
        }
        if (unregistered) {
            this.emitter.emit('steward:unregistered', stewardId);
        }
        return unregistered;
    }
    async refreshSteward(stewardId) {
        await this.unregisterSteward(stewardId);
        await this.registerSteward(stewardId);
    }
    async registerAllStewards() {
        const stewards = await this.agentRegistry.getStewards();
        let registered = 0;
        for (const steward of stewards) {
            const success = await this.registerSteward(steward.id);
            if (success) {
                registered++;
            }
        }
        logger.info(`Registered ${registered}/${stewards.length} steward(s)`);
        return registered;
    }
    // ----------------------------------------
    // Manual Execution
    // ----------------------------------------
    async executeSteward(stewardId, context) {
        const agent = await this.agentRegistry.getAgent(stewardId);
        if (!agent) {
            return {
                success: false,
                error: `Steward not found: ${stewardId}`,
                durationMs: 0,
            };
        }
        const metadata = getAgentMetadata(agent);
        if (!metadata || metadata.agentRole !== 'steward') {
            return {
                success: false,
                error: `Entity is not a steward: ${stewardId}`,
                durationMs: 0,
            };
        }
        // Create a manual trigger
        const manualTrigger = {
            type: 'event',
            event: 'manual',
        };
        return this.runExecution(agent, manualTrigger, true, context);
    }
    // ----------------------------------------
    // Event Publishing
    // ----------------------------------------
    async publishEvent(eventName, eventData) {
        if (!this.running) {
            return 0;
        }
        const subscriptions = this.eventSubscriptions.get(eventName) ?? [];
        let triggered = 0;
        for (const sub of subscriptions) {
            if (!sub.isActive) {
                continue;
            }
            // Check condition if present
            if (sub.trigger.condition) {
                const matches = evaluateCondition(sub.trigger.condition, eventData);
                if (!matches) {
                    continue;
                }
            }
            // Execute the steward
            const agent = await this.agentRegistry.getAgent(sub.stewardId);
            if (agent) {
                // Run execution asynchronously
                this.runExecution(agent, sub.trigger, false, eventData).catch((error) => {
                    logger.error(`Event-triggered execution failed for steward '${sub.stewardName}':`, error);
                });
                triggered++;
            }
        }
        return triggered;
    }
    // ----------------------------------------
    // Status & Queries
    // ----------------------------------------
    getScheduledJobs(stewardId) {
        const jobs = [];
        for (const job of this.cronJobs.values()) {
            if (stewardId && job.stewardId !== stewardId) {
                continue;
            }
            jobs.push({
                stewardId: job.stewardId,
                stewardName: job.stewardName,
                trigger: job.trigger,
                isRunning: job.isRunning,
                nextRunAt: job.nextRunAt,
                lastRunAt: job.lastRunAt,
            });
        }
        return jobs;
    }
    getEventSubscriptions(stewardId) {
        const subscriptions = [];
        for (const subs of this.eventSubscriptions.values()) {
            for (const sub of subs) {
                if (stewardId && sub.stewardId !== stewardId) {
                    continue;
                }
                subscriptions.push({
                    stewardId: sub.stewardId,
                    stewardName: sub.stewardName,
                    trigger: sub.trigger,
                    isActive: sub.isActive,
                });
            }
        }
        return subscriptions;
    }
    getExecutionHistory(filter) {
        let entries = [...this.executionHistory];
        if (filter) {
            if (filter.stewardId) {
                entries = entries.filter(e => e.stewardId === filter.stewardId);
            }
            if (filter.triggerType) {
                entries = entries.filter(e => e.trigger.type === filter.triggerType);
            }
            if (filter.success !== undefined) {
                entries = entries.filter(e => e.result?.success === filter.success);
            }
            if (filter.startedAfter) {
                entries = entries.filter(e => e.startedAt >= filter.startedAfter);
            }
            if (filter.startedBefore) {
                entries = entries.filter(e => e.startedAt <= filter.startedBefore);
            }
        }
        // Sort by startedAt descending
        entries.sort((a, b) => (b.startedAt > a.startedAt ? 1 : -1));
        if (filter?.limit) {
            entries = entries.slice(0, filter.limit);
        }
        return entries;
    }
    getLastExecution(stewardId) {
        const entries = this.getExecutionHistory({ stewardId, limit: 1 });
        return entries[0];
    }
    getStats() {
        const registeredStewards = new Set();
        for (const job of this.cronJobs.values()) {
            registeredStewards.add(job.stewardId);
        }
        for (const subs of this.eventSubscriptions.values()) {
            for (const sub of subs) {
                registeredStewards.add(sub.stewardId);
            }
        }
        const successfulExecutions = this.executionHistory.filter(e => e.result?.success === true).length;
        const failedExecutions = this.executionHistory.filter(e => e.result?.success === false).length;
        return {
            registeredStewards: registeredStewards.size,
            activeCronJobs: this.cronJobs.size,
            activeEventSubscriptions: [...this.eventSubscriptions.values()].flat().length,
            totalExecutions: this.executionHistory.length,
            successfulExecutions,
            failedExecutions,
            runningExecutions: this.runningExecutions.size,
        };
    }
    on(event, listener) {
        this.emitter.on(event, listener);
    }
    off(event, listener) {
        this.emitter.off(event, listener);
    }
    // ----------------------------------------
    // Private Helpers
    // ----------------------------------------
    startCronJob(job) {
        if (job.intervalId) {
            return; // Already running
        }
        this.scheduleNextRun(job);
    }
    scheduleNextRun(job) {
        const nextTime = this.getNextCronTime(job.trigger.schedule);
        if (!nextTime) {
            logger.warn(`Failed to compute next run time for steward '${job.stewardName}' with schedule '${job.trigger.schedule}'`);
            return;
        }
        job.nextRunAt = nextTime;
        const delayMs = Math.max(0, nextTime.getTime() - Date.now());
        logger.debug(`Scheduled next run for steward '${job.stewardName}' at ${nextTime.toISOString()} (in ${Math.round(delayMs / 1000)}s)`);
        job.intervalId = setTimeout(async () => {
            try {
                if (!this.running)
                    return;
                if (job.isRunning) {
                    logger.warn(`Skipping overlapping execution for steward '${job.stewardName}'`);
                    return; // finally block will schedule the next run
                }
                logger.debug(`Cron firing for steward '${job.stewardName}' (schedule: ${job.trigger.schedule})`);
                const agent = await this.agentRegistry.getAgent(job.stewardId);
                if (!agent) {
                    logger.warn(`Agent not found for steward '${job.stewardId}', skipping cron execution`);
                }
                if (agent && this.running) {
                    const result = await this.runExecution(agent, job.trigger, false);
                    job.lastRunAt = createTimestamp();
                    logger.info(`Cron execution completed for steward '${job.stewardName}': success=${result.success}${result.error ? `, error=${result.error}` : ''}`);
                }
            }
            catch (error) {
                logger.error(`Unhandled error in cron callback for steward '${job.stewardName}':`, error);
            }
            finally {
                if (this.running) {
                    job.intervalId = undefined;
                    this.scheduleNextRun(job);
                }
            }
        }, delayMs);
    }
    stopCronJob(job) {
        if (job.intervalId) {
            clearTimeout(job.intervalId);
            job.intervalId = undefined;
        }
    }
    /**
     * Calculates the next fire time for a cron expression after the given date.
     * Supports 5-field cron (minute hour dom month dow) with optional 6th seconds field (ignored).
     * Supports: *, specific values, step (* /N), range (N-M), and lists (N,M).
     * Returns null for invalid expressions.
     */
    getNextCronTime(schedule, after) {
        const parts = schedule.trim().split(/\s+/);
        if (parts.length < 5 || parts.length > 6) {
            return null;
        }
        // If 6 fields, ignore the first (seconds) field
        const fields = parts.length === 6 ? parts.slice(1) : parts;
        const [minuteField, hourField, domField, monthField, dowField] = fields;
        // Parse each field into a set of allowed values
        const minutes = this.parseCronField(minuteField, 0, 59);
        const hours = this.parseCronField(hourField, 0, 23);
        const doms = this.parseCronField(domField, 1, 31);
        const months = this.parseCronField(monthField, 1, 12);
        const dows = this.parseCronField(dowField, 0, 6); // 0=Sunday
        if (!minutes || !hours || !doms || !months || !dows) {
            return null;
        }
        // Start from the next minute after 'after'
        const start = after ? new Date(after) : new Date();
        start.setSeconds(0, 0);
        start.setMinutes(start.getMinutes() + 1);
        // Iterate forward up to 366 days to find the next matching time
        const maxIterations = 366 * 24 * 60; // worst case: check every minute for a year
        const candidate = new Date(start);
        for (let i = 0; i < maxIterations; i++) {
            const month = candidate.getMonth() + 1; // 1-12
            const dom = candidate.getDate();
            const dow = candidate.getDay(); // 0=Sunday
            const hour = candidate.getHours();
            const minute = candidate.getMinutes();
            if (months.has(month) &&
                doms.has(dom) &&
                dows.has(dow) &&
                hours.has(hour) &&
                minutes.has(minute)) {
                return candidate;
            }
            // Advance by 1 minute
            candidate.setMinutes(candidate.getMinutes() + 1);
        }
        return null; // No match found within 366 days
    }
    /**
     * Parses a single cron field into a set of valid values.
     * Supports: *, N, N-M, N,M, * /N, N-M/S
     */
    parseCronField(field, min, max) {
        const result = new Set();
        // Handle comma-separated list
        const segments = field.split(',');
        for (const segment of segments) {
            // Handle step: */N or N-M/S
            const stepMatch = segment.match(/^(.+)\/(\d+)$/);
            if (stepMatch) {
                const step = parseInt(stepMatch[2], 10);
                if (step <= 0)
                    return null;
                let rangeStart = min;
                let rangeEnd = max;
                if (stepMatch[1] !== '*') {
                    const rangeMatch = stepMatch[1].match(/^(\d+)-(\d+)$/);
                    if (rangeMatch) {
                        rangeStart = parseInt(rangeMatch[1], 10);
                        rangeEnd = parseInt(rangeMatch[2], 10);
                    }
                    else {
                        rangeStart = parseInt(stepMatch[1], 10);
                        rangeEnd = max;
                    }
                }
                for (let v = rangeStart; v <= rangeEnd; v += step) {
                    if (v >= min && v <= max)
                        result.add(v);
                }
                continue;
            }
            // Handle wildcard
            if (segment === '*') {
                for (let v = min; v <= max; v++)
                    result.add(v);
                continue;
            }
            // Handle range: N-M
            const rangeMatch = segment.match(/^(\d+)-(\d+)$/);
            if (rangeMatch) {
                const start = parseInt(rangeMatch[1], 10);
                const end = parseInt(rangeMatch[2], 10);
                for (let v = start; v <= end; v++) {
                    if (v >= min && v <= max)
                        result.add(v);
                }
                continue;
            }
            // Handle specific value
            const value = parseInt(segment, 10);
            if (isNaN(value) || value < min || value > max)
                return null;
            result.add(value);
        }
        return result.size > 0 ? result : null;
    }
    async runExecution(agent, trigger, manual, eventContext) {
        const executionId = `exec-${++this.executionCounter}-${Date.now()}`;
        const stewardId = agent.id;
        const startedAt = createTimestamp();
        const entry = {
            executionId,
            stewardId,
            stewardName: agent.name,
            trigger,
            manual,
            startedAt,
            eventContext,
        };
        this.addHistoryEntry(entry);
        this.runningExecutions.add(executionId);
        this.emitter.emit('execution:started', entry);
        // Update job state if this is a cron job
        for (const job of this.cronJobs.values()) {
            if (job.stewardId === stewardId && isCronTrigger(trigger)) {
                job.isRunning = true;
            }
        }
        const startTime = Date.now();
        try {
            // Execute the steward
            const result = await this.executor(agent, { trigger, eventContext });
            // Complete the entry
            const completedEntry = {
                ...entry,
                completedAt: createTimestamp(),
                result,
            };
            this.updateHistoryEntry(executionId, completedEntry);
            this.runningExecutions.delete(executionId);
            if (result.success) {
                this.emitter.emit('execution:completed', completedEntry);
            }
            else {
                this.emitter.emit('execution:failed', completedEntry);
            }
            // Update job state
            for (const job of this.cronJobs.values()) {
                if (job.stewardId === stewardId) {
                    job.isRunning = false;
                    job.lastRunAt = completedEntry.completedAt;
                }
            }
            // Update steward metadata with last execution time
            await this.agentRegistry.updateAgentMetadata(stewardId, {
                lastExecutedAt: completedEntry.completedAt,
            });
            return result;
        }
        catch (error) {
            const errorMessage = error instanceof Error ? error.message : String(error);
            const result = {
                success: false,
                error: errorMessage,
                durationMs: Date.now() - startTime,
            };
            const failedEntry = {
                ...entry,
                completedAt: createTimestamp(),
                result,
            };
            this.updateHistoryEntry(executionId, failedEntry);
            this.runningExecutions.delete(executionId);
            this.emitter.emit('execution:failed', failedEntry);
            // Update job state
            for (const job of this.cronJobs.values()) {
                if (job.stewardId === stewardId) {
                    job.isRunning = false;
                }
            }
            return result;
        }
    }
    addHistoryEntry(entry) {
        this.executionHistory.push(entry);
        this.pruneHistory();
    }
    updateHistoryEntry(executionId, entry) {
        const index = this.executionHistory.findIndex(e => e.executionId === executionId);
        if (index !== -1) {
            this.executionHistory[index] = entry;
        }
    }
    pruneHistory() {
        // Group by steward and keep only maxHistoryPerSteward entries per steward
        const bysteward = new Map();
        for (const entry of this.executionHistory) {
            if (!bysteward.has(entry.stewardId)) {
                bysteward.set(entry.stewardId, []);
            }
            bysteward.get(entry.stewardId).push(entry);
        }
        const prunedHistory = [];
        for (const entries of bysteward.values()) {
            // Sort by startedAt descending and keep only the first maxHistoryPerSteward
            entries.sort((a, b) => (b.startedAt > a.startedAt ? 1 : -1));
            prunedHistory.push(...entries.slice(0, this.config.maxHistoryPerSteward));
        }
        this.executionHistory = prunedHistory;
    }
}
// ============================================================================
// Factory Function
// ============================================================================
/**
 * Creates a StewardScheduler instance
 *
 * @param agentRegistry - The agent registry for looking up stewards
 * @param executor - Function to execute steward logic
 * @param config - Optional configuration
 */
export function createStewardScheduler(agentRegistry, executor, config) {
    return new StewardSchedulerImpl(agentRegistry, executor, config);
}
// ============================================================================
// Steward Executor Factory
// ============================================================================
/**
 * Default idle timeout for steward sessions in milliseconds (2 minutes).
 * If a steward session receives no events for this duration, it is
 * assumed to be stuck and will be force-terminated.
 */
export const STEWARD_SESSION_DEFAULT_IDLE_TIMEOUT_MS = 120_000;
/**
 * Default maximum duration for steward sessions in milliseconds (30 minutes).
 * Steward sessions exceeding this duration are force-terminated regardless
 * of activity, as a safety net.
 */
export const STEWARD_SESSION_DEFAULT_MAX_DURATION_MS = 30 * 60 * 1000;
/**
 * Sets up idle timeout and max duration monitoring for a spawned steward session.
 * Watches for session activity via the session manager's event emitter and
 * force-terminates the session if it goes idle or exceeds the max duration.
 *
 * @param sessionId - The session ID to monitor
 * @param sessionEvents - The session's event emitter (from startSession)
 * @param deps - Steward executor dependencies (for session manager and config)
 * @param stewardName - Name of the steward (for logging)
 */
function monitorStewardSession(sessionId, sessionEvents, deps, stewardName) {
    const idleTimeoutMs = deps.stewardSessionIdleTimeoutMs ?? STEWARD_SESSION_DEFAULT_IDLE_TIMEOUT_MS;
    const maxDurationMs = deps.stewardSessionMaxDurationMs ?? STEWARD_SESSION_DEFAULT_MAX_DURATION_MS;
    const startTime = Date.now();
    let lastActivityAt = Date.now();
    let cleanedUp = false;
    // Update last activity on any session event
    const onEvent = () => {
        lastActivityAt = Date.now();
    };
    // Clean up watchers
    const cleanup = () => {
        if (cleanedUp)
            return;
        cleanedUp = true;
        clearInterval(checkInterval);
        sessionEvents.removeListener('event', onEvent);
        sessionEvents.removeListener('exit', onExit);
        sessionEvents.removeListener('status', onStatus);
    };
    // Session exited naturally — clean up watchers
    const onExit = () => {
        cleanup();
    };
    // Session status changed to terminated — clean up watchers
    const onStatus = (status) => {
        if (status === 'terminated') {
            cleanup();
        }
    };
    sessionEvents.on('event', onEvent);
    sessionEvents.on('exit', onExit);
    sessionEvents.on('status', onStatus);
    // Periodically check idle time and max duration
    const checkIntervalMs = Math.min(idleTimeoutMs / 2, 30_000);
    const checkInterval = setInterval(() => {
        const now = Date.now();
        const idleMs = now - lastActivityAt;
        const totalMs = now - startTime;
        if (idleMs > idleTimeoutMs) {
            logger.warn(`Steward '${stewardName}' session ${sessionId} idle for ${Math.round(idleMs / 1000)}s (timeout: ${Math.round(idleTimeoutMs / 1000)}s), force-terminating`);
            cleanup();
            deps.sessionManager.stopSession(sessionId, {
                graceful: false,
                reason: `Steward session idle for ${Math.round(idleMs / 1000)}s (timeout: ${Math.round(idleTimeoutMs / 1000)}s)`,
            }).catch((err) => {
                const msg = err instanceof Error ? err.message : String(err);
                if (!msg.includes('not found')) {
                    logger.warn(`Failed to stop idle steward session ${sessionId}:`, err);
                }
            });
            return;
        }
        if (totalMs > maxDurationMs) {
            logger.warn(`Steward '${stewardName}' session ${sessionId} exceeded max duration ${Math.round(maxDurationMs / 1000)}s, force-terminating`);
            cleanup();
            deps.sessionManager.stopSession(sessionId, {
                graceful: false,
                reason: `Steward session exceeded max duration (${Math.round(totalMs / 1000)}s)`,
            }).catch((err) => {
                const msg = err instanceof Error ? err.message : String(err);
                if (!msg.includes('not found')) {
                    logger.warn(`Failed to stop over-duration steward session ${sessionId}:`, err);
                }
            });
        }
    }, checkIntervalMs);
    // Prevent the interval from keeping the process alive
    if (checkInterval.unref) {
        checkInterval.unref();
    }
}
/**
 * Creates a steward executor that dispatches to the appropriate service
 * based on the steward's focus.
 *
 * - 'merge' focus → MergeStewardService.processAllPending()
 * - 'docs' → spawns an agent session via sessionManager
 *
 * Session-based stewards check for an existing active session before spawning
 * to prevent overlapping runs across cron ticks.
 *
 * Each case is wrapped in try/catch so one failing steward doesn't crash
 * the scheduler.
 */
export function createStewardExecutor(deps) {
    return async (steward, _context) => {
        const metadata = getAgentMetadata(steward);
        const focus = metadata?.stewardFocus;
        const startTime = Date.now();
        switch (focus) {
            case 'merge': {
                try {
                    const result = await deps.mergeStewardService.processAllPending();
                    return {
                        success: true,
                        output: `Processed ${result.totalProcessed} tasks (${result.mergedCount} merged, ${result.errorCount} failed)`,
                        durationMs: Date.now() - startTime,
                        itemsProcessed: result.totalProcessed,
                    };
                }
                catch (error) {
                    return {
                        success: false,
                        error: error instanceof Error ? error.message : String(error),
                        output: `Merge steward '${steward.name}' failed: ${error instanceof Error ? error.message : String(error)}`,
                        durationMs: Date.now() - startTime,
                        itemsProcessed: 0,
                    };
                }
            }
            case 'docs': {
                try {
                    const stewardId = steward.id;
                    const activeSession = deps.sessionManager.getActiveSession(stewardId);
                    if (activeSession) {
                        return {
                            success: true,
                            output: `Steward '${steward.name}' already has active session ${activeSession.id}, skipping`,
                            durationMs: Date.now() - startTime,
                            itemsProcessed: 0,
                        };
                    }
                    const roleResult = loadRolePrompt('steward', focus, {
                        projectRoot: deps.projectRoot,
                    });
                    const initialPrompt = roleResult?.prompt ?? '';
                    const { session, events } = await deps.sessionManager.startSession(stewardId, {
                        workingDirectory: deps.projectRoot,
                        initialPrompt,
                        interactive: false,
                    });
                    // Monitor session for idle timeout and max duration
                    monitorStewardSession(session.id, events, deps, steward.name);
                    return {
                        success: true,
                        output: `Spawned ${focus} steward session ${session.id}`,
                        durationMs: Date.now() - startTime,
                        itemsProcessed: 1,
                    };
                }
                catch (error) {
                    return {
                        success: false,
                        error: error instanceof Error ? error.message : String(error),
                        output: `${focus} steward '${steward.name}' failed: ${error instanceof Error ? error.message : String(error)}`,
                        durationMs: Date.now() - startTime,
                        itemsProcessed: 0,
                    };
                }
            }
            case 'custom': {
                try {
                    const stewardId = steward.id;
                    const activeSession = deps.sessionManager.getActiveSession(stewardId);
                    if (activeSession) {
                        return {
                            success: true,
                            output: `Steward '${steward.name}' already has active session ${activeSession.id}, skipping`,
                            durationMs: Date.now() - startTime,
                            itemsProcessed: 0,
                        };
                    }
                    // Build prompt from steward base + custom playbook
                    // Resolve playbook content: prefer playbookId (template reference), fall back to inline playbook
                    let playbook;
                    if (metadata?.playbookId && deps.resolvePlaybookContent) {
                        try {
                            playbook = await deps.resolvePlaybookContent(metadata.playbookId);
                            if (!playbook) {
                                logger.warn(`Custom steward '${steward.name}': playbook template ${metadata.playbookId} not found, falling back to inline playbook`);
                                playbook = metadata?.playbook;
                            }
                        }
                        catch (err) {
                            logger.warn(`Custom steward '${steward.name}': failed to resolve playbook template ${metadata.playbookId}: ${err}, falling back to inline playbook`);
                            playbook = metadata?.playbook;
                        }
                    }
                    else {
                        playbook = metadata?.playbook;
                    }
                    if (!playbook) {
                        return {
                            success: false,
                            error: 'Custom steward has no playbook configured',
                            output: `Custom steward '${steward.name}' has no playbook configured`,
                            durationMs: Date.now() - startTime,
                            itemsProcessed: 0,
                        };
                    }
                    // Load the steward base prompt for shared context
                    const roleResult = loadRolePrompt('steward', undefined, {
                        projectRoot: deps.projectRoot,
                    });
                    const basePrompt = roleResult?.prompt ?? '';
                    // Combine base steward prompt with the custom playbook
                    const initialPrompt = basePrompt
                        ? `${basePrompt}\n\n---\n\n## Custom Steward Playbook\n\n${playbook}`
                        : `## Custom Steward Playbook\n\n${playbook}`;
                    const { session, events } = await deps.sessionManager.startSession(stewardId, {
                        workingDirectory: deps.projectRoot,
                        initialPrompt,
                        interactive: false,
                    });
                    // Monitor session for idle timeout and max duration
                    monitorStewardSession(session.id, events, deps, steward.name);
                    return {
                        success: true,
                        output: `Spawned custom steward session ${session.id}`,
                        durationMs: Date.now() - startTime,
                        itemsProcessed: 1,
                    };
                }
                catch (error) {
                    return {
                        success: false,
                        error: error instanceof Error ? error.message : String(error),
                        output: `Custom steward '${steward.name}' failed: ${error instanceof Error ? error.message : String(error)}`,
                        durationMs: Date.now() - startTime,
                        itemsProcessed: 0,
                    };
                }
            }
            default:
                return {
                    success: false,
                    output: `Unknown steward focus: ${focus}`,
                    durationMs: Date.now() - startTime,
                    itemsProcessed: 0,
                };
        }
    };
}
// ============================================================================
// Default Executor (deprecated — use createStewardExecutor)
// ============================================================================
/**
 * Creates a default steward executor that returns fake success without
 * calling any real service. Useful for tests.
 *
 * @deprecated Use {@link createStewardExecutor} instead, which dispatches
 * to real steward services based on the steward's focus.
 */
export function createDefaultStewardExecutor() {
    return async (steward, _context) => {
        const metadata = getAgentMetadata(steward);
        const focus = metadata?.stewardFocus;
        // Stub - returns success with info about what would be done
        return {
            success: true,
            output: `Steward ${steward.name} (${focus}) executed successfully`,
            durationMs: 0,
            itemsProcessed: 0,
        };
    };
}
//# sourceMappingURL=steward-scheduler.js.map