/**
 * Heartbeat task runner service.
 *
 * Owns the execution of one durable heartbeat task: checkpoint loading, task
 * state transitions, runner-agent invocation, checkpoint persistence, and run
 * history persistence. The scheduler decides when a task is due; this service
 * decides how that task is executed and recorded.
 */
import dayjs from 'dayjs';
import { ToolApprovalPolicies } from '../../../core/approvals/index.js';
import { DEFAULT_OPENAI_MODEL } from '../../../core/config.js';
import { RuntimeCredentialService } from '../../../core/runtime/credentials/index.js';
import { HeartbeatRunnerAgent } from '../agent/index.js';
import { HeartbeatTaskStateProjector } from '../tasks/index.js';
const DEFAULT_FAILURE_RETRY_MS = 5 * 60_000;
export class HeartbeatTaskRunnerService {
    // Runs one already-selected task and persists the resulting task state, checkpoint, and run record.
    static async runTask(options) {
        const { task, runAt } = options;
        const timestamp = dayjs(runAt).toISOString();
        try {
            const checkpoint = await options.store.loadCheckpoint(task);
            const loadedCheckpoint = Boolean(checkpoint);
            const runningTask = HeartbeatTaskStateProjector.markRunning({
                task,
                now: runAt,
                loadedCheckpoint,
            });
            await options.store.saveTask(runningTask);
            options.onEvent?.(HeartbeatTaskRunnerService.startedEvent(runningTask, loadedCheckpoint, timestamp));
            const result = await HeartbeatTaskRunnerService.runAgent({
                task,
                checkpoint,
                runAt,
                runner: options.runner,
                runtime: options.runtime,
                onEvent: options.onEvent,
            });
            await options.store.saveCheckpoint(task, result.checkpoint);
            const nextTask = HeartbeatTaskStateProjector.afterResult({
                task,
                result,
                now: runAt,
                loadedCheckpoint,
            });
            await options.store.saveTask(nextTask);
            const record = { task: nextTask, result, loadedCheckpoint };
            await options.store.saveRunRecord?.(record);
            options.onEvent?.({
                type: 'heartbeat.task.finished',
                taskId: task.id,
                record,
                timestamp,
            });
            return { record, failed: false };
        }
        catch (error) {
            const nextTask = HeartbeatTaskStateProjector.afterFailure({
                task,
                error,
                now: runAt,
                retryMs: options.failureRetryMs ?? DEFAULT_FAILURE_RETRY_MS,
            });
            await options.store.saveTask(nextTask);
            options.onEvent?.(HeartbeatTaskRunnerService.failedEvent(nextTask, error, timestamp));
            return { failed: true };
        }
    }
    // Runs one task by id for operator-triggered paths such as web-v2 "Run now".
    static async runTaskById(options) {
        const now = options.now?.() ?? dayjs().toDate();
        const tasks = await options.store.listTasks();
        const task = tasks.find((candidate) => candidate.id === options.taskId);
        if (!task) {
            throw new Error(`Heartbeat task not found: ${options.taskId}`);
        }
        if (!task.enabled) {
            throw new Error(`Heartbeat task ${options.taskId} is disabled. Enable it before running.`);
        }
        const result = await HeartbeatTaskRunnerService.runTask({ ...options, task, runAt: now });
        return {
            checked: 1,
            ran: result.record ? 1 : 0,
            failed: result.failed ? 1 : 0,
            records: result.record ? [result.record] : [],
        };
    }
    static async runAgent(args) {
        return args.runner ?
            await args.runner(args.task, args.checkpoint)
            : await HeartbeatRunnerAgent.run(HeartbeatTaskRunnerService.resolveRunnerAgentOptions(args));
    }
    static resolveRunnerAgentOptions(args) {
        const model = args.task.runtime?.model ?? args.runtime?.model ?? process.env.OPENAI_MODEL ?? process.env.ANTHROPIC_MODEL ?? DEFAULT_OPENAI_MODEL;
        const credentialOptions = {
            apiKey: args.runtime?.apiKey,
            apiKeyProvider: args.runtime?.apiKeyProvider,
            preferApiKey: args.runtime?.preferApiKey,
        };
        if (!RuntimeCredentialService.hasCredentialForModel(model, credentialOptions)) {
            throw new Error(RuntimeCredentialService.formatMissingCredentialMessage(model));
        }
        return {
            ...args.runtime,
            ...args.task.runtime,
            task: args.task.task,
            checkpoint: args.checkpoint,
            runContext: {
                currentDateTime: dayjs(args.runAt).toISOString(),
                intervalMs: args.task.schedule.intervalMs,
                continuationMode: args.task.continuationMode ?? 'operator',
                nextRunAt: args.task.schedule.nextRunAt,
                previousRunAt: args.task.state?.runAt,
                previousRunId: args.task.state?.runId,
            },
            model,
            apiKey: RuntimeCredentialService.resolveApiKeyForModel(model, credentialOptions),
            workspaceRoot: args.runtime?.workspaceRoot ?? args.task.runtime?.workspaceRoot,
            stateDir: args.runtime?.stateDir ?? args.task.runtime?.stateDir,
            memoryDir: args.runtime?.memoryDir ?? args.task.runtime?.memoryDir,
            approvalPolicies: [
                ToolApprovalPolicies.unattendedLocalAutomation(),
                ...(args.runtime?.approvalPolicies ?? []),
            ],
            approveToolCall: HeartbeatTaskRunnerService.denyInteractiveToolCall,
            onEvent: (event) => {
                args.runtime?.onAgentEvent?.(event);
                args.onEvent?.({
                    type: 'heartbeat.task.agent_event',
                    taskId: args.task.id,
                    event,
                    timestamp: 'timestamp' in event && typeof event.timestamp === 'string' ? event.timestamp : dayjs().toISOString(),
                });
            },
        };
    }
    static startedEvent(task, loadedCheckpoint, timestamp) {
        return {
            type: 'heartbeat.task.started',
            taskId: task.id,
            loadedCheckpoint,
            status: task.state?.status ?? 'running',
            progress: task.state?.progress ?? '',
            timestamp,
        };
    }
    static failedEvent(task, error, timestamp) {
        return {
            type: 'heartbeat.task.failed',
            taskId: task.id,
            error: error instanceof Error ? error.message : String(error),
            status: task.state?.status ?? 'failed',
            progress: task.state?.progress ?? '',
            nextRunAt: task.schedule.nextRunAt,
            timestamp,
        };
    }
    static async denyInteractiveToolCall(call) {
        return {
            approved: false,
            reason: `Heartbeat task cannot request live approval for ${call.tool}.`,
        };
    }
}
//# sourceMappingURL=runner.js.map