import { and, eq, inArray, lte, sql } from "drizzle-orm";
import { agents, agentWakeupRequests, activityLog, heartbeatRuns, issues, } from "@rudderhq/db";
import { conflict } from "../../errors.js";
import { publishLiveEvent } from "../live-events.js";
import { parseObject, asBoolean, asNumber } from "../../agent-runtimes/utils.js";
export { prioritizeProjectWorkspaceCandidatesForRun } from "../agent-run-context.js";
import * as heartbeatCore from "./heartbeat.core.js";
import * as heartbeatSessions from "./heartbeat.sessions.js";
const { MAX_LIVE_LOG_CHUNK_BYTES, HEARTBEAT_MAX_CONCURRENT_RUNS_DEFAULT, HEARTBEAT_MAX_CONCURRENT_RUNS_MIN, HEARTBEAT_MAX_CONCURRENT_RUNS_MAX, DEFERRED_WAKE_CONTEXT_KEY, DETACHED_PROCESS_ERROR_CODE, ORPHANED_PROCESS_TERMINATION_GRACE_MS, ORPHANED_PROCESS_KILL_WAIT_MS, ORPHANED_PROCESS_POLL_INTERVAL_MS, startLocksByAgent, MAX_RECOVERY_CHAIN_DEPTH, ISSUE_PASSIVE_FOLLOWUP_REASON, ISSUE_PASSIVE_FOLLOWUP_WAKE_SOURCE, ISSUE_PASSIVE_FOLLOWUP_FAILURE_REASON, ISSUE_PASSIVE_FOLLOWUP_MAX_ATTEMPTS, ISSUE_REVIEW_CLOSEOUT_REASON, ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON, ISSUE_REVIEW_CLOSEOUT_MAX_ATTEMPTS, ISSUE_PASSIVE_FOLLOWUP_COOLDOWN_MS_BY_ATTEMPT, ISSUE_PASSIVE_FOLLOWUP_TIMER_CONTINUITY_MAX_WINDOW_MS, SESSIONED_LOCAL_ADAPTERS, heartbeatRunListColumns, appendExcerpt, appendTranscriptEntriesFromChunk, normalizeMaxConcurrentRuns, withAgentStartLock, readNonEmptyString, resolveHeartbeatObservabilitySurface, buildHeartbeatObservationName, compactTraceText, buildIssueRunTraceName, buildHeartbeatRuntimeTraceMetadata, buildHeartbeatAdapterInvokePayload, buildRecentDateKeys, buildDateKeysBetween, fallbackSkillLabel, normalizeLoadedSkill, normalizeLoadedSkillForPayload, emptySkillEvidenceCounts, incrementSkillEvidenceCount, strongestSkillEvidence, resolveSkillEvidence, readSkillEvidenceFromPayload, extractSkillSlugFromPath, collectSkillPathsFromText, collectStringValues, normalizeSkillUseFromPath, dedupeSkillUses, collectSkillUsesFromText, readToolCommandInput, isCommandTranscriptTool, isReadTranscriptTool, inferUsedSkillsFromTranscript, normalizeSkillCandidate, addSkillCandidate, readSkillReferenceSlug, collectSkillReferences, inferUsedSkillsFromPrompt, normalizeLedgerBillingType, resolveLedgerBiller, normalizeBilledCostCents, resolveLedgerScopeForRun } = heartbeatCore;
const { buildExplicitResumeSessionOverride, normalizeUsageTotals, readRawUsageTotals, deriveNormalizedUsageDelta, formatCount, parseSessionCompactionPolicy, resolveRuntimeSessionParamsForWorkspace, parseIssueAssigneeAgentRuntimeOverrides, deriveTaskKey, shouldResetTaskSessionForWake, formatRuntimeWorkspaceWarningLog, describeSessionResetReason, deriveCommentId, enrichWakeContextSnapshot, mergeCoalescedContextSnapshot, issueCommentAuthorKind, issueCommentAuthorLabel, buildDeferredWakePayload, readDeferredWakeContext, readDeferredWakePayload, deriveDeferredWakeTaskKey, hydrateWakeContextSnapshot, firstNonEmptyLine, deriveRecoveryFailureKind, deriveRecoveryFailureSummary, mergeMissingRecoveryContextFields, hydrateRecoveryBaseContextSnapshot, buildRecoveryContextSnapshot, normalizePassiveFollowupContext, normalizeReviewCloseoutContext, passiveFollowupCooldownMs, issueHasReviewer, isAgentEligibleForTimerContinuation, hasCredibleTimerContinuation, buildPassiveFollowupContextSnapshot, runTaskKey, isSameTaskScope, isTrackedLocalChildProcessAdapter, isProcessAlive, waitForProcessExit, terminateOrphanedProcess, truncateDisplayId, normalizeAgentNameKey, defaultSessionCodec, getAgentRuntimeSessionCodec, normalizeSessionParams, resolveNextSessionState } = heartbeatSessions;
export function createHeartbeatRecoveryHandlers(context) {
    const { db, instanceSettings, getCurrentUserRedactionOptions, runLogStore, runContextSvc, issuesSvc, documentsSvc, executionWorkspacesSvc, workspaceOperationsSvc, activeRunExecutions, budgetHooks, budgets, getAgent, getRun, getRuntimeState, getTaskSession, getLatestRunForSession, getOldestRunForSession, resolveNormalizedUsageForSession, evaluateSessionCompaction, resolveSessionBeforeForWakeup, resolveExplicitResumeSessionOverride, upsertTaskSession, clearTaskSessions, ensureRuntimeState, buildHeartbeatObservabilityContext, emitHeartbeatObservationEvent, emitHeartbeatLiveEval, setRunStatus, setWakeupStatus, updateWakeupRequestRecord, insertWakeupRequestRecord, appendRunEvent, nextRunEventSeq, persistRunProcessMetadata, clearDetachedRunWarning, countRunningRunsForAgent, claimQueuedRun, finalizeAgentStatus, reapOrphanedRuns, resumeQueuedRuns, updateRuntimeState, startNextQueuedRunForAgent, executeRun, releaseIssueExecutionAndPromote, enqueueWakeup, resumeDeferredWakeupsForAgent, listProjectScopedRunIds, listProjectScopedWakeupIds, cancelPendingWakeupsForBudgetScope, cancelRunInternal, cancelActiveForAgentInternal, cancelBudgetScopeWork, retryRunInternal, buildSkillAnalytics } = context;
    async function enqueueRecoveryRun(run, agent, opts) {
        /**
         * Recovery runs intentionally clone the prior run's task context and then
         * layer explicit recovery metadata on top. This keeps retries visible and
         * auditable while preserving "continue preferred" semantics for issue work.
         *
         * Reasoning:
         * - Manual retry and automatic process-loss retry must assemble the same
         *   recovery contract so prompts/runtime behavior stay aligned.
         * - We backfill missing context from the retry chain to recover from older
         *   lossy retry runs without mutating the historical source run rows.
         *
         * Traceability:
         * - doc/developing/RUN-RECOVERY.md
         * - doc/DEVELOPING.md
         */
        const baseContextSnapshot = await hydrateRecoveryBaseContextSnapshot(run, getRun);
        const recoveryContextSnapshot = buildRecoveryContextSnapshot({
            baseContextSnapshot,
            run,
            recoveryTrigger: opts.recoveryTrigger,
            wakeReason: opts.wakeReason,
            wakeSource: `recovery.${opts.recoveryTrigger}`,
            triggerDetail: opts.triggerDetail,
        });
        const issueId = readNonEmptyString(recoveryContextSnapshot.issueId);
        const taskKey = deriveTaskKey(recoveryContextSnapshot, null);
        const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
        const recovery = recoveryContextSnapshot.recovery;
        const requestPayload = {
            originalRunId: run.id,
            failureKind: recovery.failureKind,
            recoveryTrigger: recovery.recoveryTrigger,
            ...(issueId ? { issueId } : {}),
        };
        const outcome = await db.transaction(async (tx) => {
            let issueRow = null;
            if (issueId) {
                await tx.execute(sql `select id from issues where id = ${issueId} and org_id = ${run.orgId} for update`);
                issueRow = await tx
                    .select({
                    id: issues.id,
                    orgId: issues.orgId,
                    executionRunId: issues.executionRunId,
                    executionAgentNameKey: issues.executionAgentNameKey,
                })
                    .from(issues)
                    .where(and(eq(issues.id, issueId), eq(issues.orgId, run.orgId)))
                    .then((rows) => rows[0] ?? null);
            }
            if (issueRow?.executionRunId) {
                const activeExecutionRun = await tx
                    .select()
                    .from(heartbeatRuns)
                    .where(eq(heartbeatRuns.id, issueRow.executionRunId))
                    .then((rows) => rows[0] ?? null);
                const isActiveExecutionRun = activeExecutionRun &&
                    (activeExecutionRun.status === "queued" || activeExecutionRun.status === "running");
                if (!isActiveExecutionRun) {
                    await tx
                        .update(issues)
                        .set({
                        executionRunId: null,
                        executionAgentNameKey: null,
                        executionLockedAt: null,
                        updatedAt: opts.now,
                    })
                        .where(eq(issues.id, issueRow.id));
                    issueRow = {
                        ...issueRow,
                        executionRunId: null,
                        executionAgentNameKey: null,
                    };
                }
                else if (activeExecutionRun) {
                    const activeContext = parseObject(activeExecutionRun.contextSnapshot);
                    const activeRecovery = parseObject(activeContext.recovery);
                    if (activeExecutionRun.agentId === run.agentId &&
                        (activeExecutionRun.retryOfRunId === run.id ||
                            readNonEmptyString(activeRecovery.originalRunId) === run.id)) {
                        return { kind: "existing", run: activeExecutionRun };
                    }
                    throw conflict("Issue already has an active execution run", {
                        issueId: issueRow.id,
                        executionRunId: activeExecutionRun.id,
                    });
                }
            }
            const wakeupRequest = await tx
                .insert(agentWakeupRequests)
                .values({
                orgId: run.orgId,
                agentId: run.agentId,
                source: opts.source,
                triggerDetail: opts.triggerDetail,
                reason: opts.wakeReason,
                payload: requestPayload,
                status: "queued",
                requestedByActorType: opts.requestedByActorType ?? null,
                requestedByActorId: opts.requestedByActorId ?? null,
                updatedAt: opts.now,
            })
                .returning()
                .then((rows) => rows[0]);
            const recoveryRun = await tx
                .insert(heartbeatRuns)
                .values({
                orgId: run.orgId,
                agentId: run.agentId,
                invocationSource: opts.source,
                triggerDetail: opts.triggerDetail,
                status: "queued",
                wakeupRequestId: wakeupRequest.id,
                contextSnapshot: recoveryContextSnapshot,
                sessionIdBefore: sessionBefore,
                retryOfRunId: run.id,
                processLossRetryCount: opts.recoveryTrigger === "automatic"
                    ? (run.processLossRetryCount ?? 0) + 1
                    : (run.processLossRetryCount ?? 0),
                updatedAt: opts.now,
            })
                .returning()
                .then((rows) => rows[0]);
            await tx
                .update(agentWakeupRequests)
                .set({
                runId: recoveryRun.id,
                updatedAt: opts.now,
            })
                .where(eq(agentWakeupRequests.id, wakeupRequest.id));
            if (issueRow) {
                await tx
                    .update(issues)
                    .set({
                    executionRunId: recoveryRun.id,
                    executionAgentNameKey: normalizeAgentNameKey(agent.name),
                    executionLockedAt: opts.now,
                    updatedAt: opts.now,
                })
                    .where(eq(issues.id, issueRow.id));
            }
            return { kind: "queued", run: recoveryRun };
        });
        if (outcome.kind === "existing")
            return outcome.run;
        const recoveryRun = outcome.run;
        await appendRunEvent(recoveryRun, await nextRunEventSeq(recoveryRun.id), {
            eventType: "lifecycle",
            stream: "system",
            level: opts.recoveryTrigger === "automatic" ? "warn" : "info",
            message: `Recovery queued from run ${run.id}`,
            payload: {
                originalRunId: run.id,
                failureKind: recovery.failureKind,
                failureSummary: recovery.failureSummary,
                recoveryTrigger: recovery.recoveryTrigger,
                recoveryMode: recovery.recoveryMode,
            },
        });
        publishLiveEvent({
            orgId: recoveryRun.orgId,
            type: "heartbeat.run.queued",
            payload: {
                runId: recoveryRun.id,
                agentId: recoveryRun.agentId,
                invocationSource: recoveryRun.invocationSource,
                triggerDetail: recoveryRun.triggerDetail,
                wakeupRequestId: recoveryRun.wakeupRequestId,
            },
        });
        if (opts.startImmediately !== false) {
            await startNextQueuedRunForAgent(agent.id);
        }
        return recoveryRun;
    }
    async function enqueueProcessLossRetry(run, agent, now) {
        return enqueueRecoveryRun(run, agent, {
            recoveryTrigger: "automatic",
            source: "automation",
            triggerDetail: "system",
            wakeReason: "process_lost_retry",
            requestedByActorType: "system",
            requestedByActorId: null,
            startImmediately: false,
            now,
        });
    }
    function parseHeartbeatPolicy(agent) {
        const runtimeConfig = parseObject(agent.runtimeConfig);
        const heartbeat = parseObject(runtimeConfig.heartbeat);
        return {
            enabled: asBoolean(heartbeat.enabled, true),
            intervalSec: Math.max(0, asNumber(heartbeat.intervalSec, 0)),
            wakeOnDemand: asBoolean(heartbeat.wakeOnDemand ?? heartbeat.wakeOnAssignment ?? heartbeat.wakeOnOnDemand ?? heartbeat.wakeOnAutomation, true),
            preflightEnabled: asBoolean(heartbeat.preflightEnabled ?? heartbeat.timerPreflightEnabled, true),
            maxConcurrentRuns: normalizeMaxConcurrentRuns(heartbeat.maxConcurrentRuns),
        };
    }
    async function markAgentHeartbeatChecked(agent, outcome) {
        const now = new Date();
        const updated = await db
            .update(agents)
            .set({
            lastHeartbeatAt: now,
            updatedAt: now,
        })
            .where(eq(agents.id, agent.id))
            .returning()
            .then((rows) => rows[0] ?? null);
        if (updated) {
            publishLiveEvent({
                orgId: updated.orgId,
                type: "agent.status",
                payload: {
                    agentId: updated.id,
                    status: updated.status,
                    lastHeartbeatAt: updated.lastHeartbeatAt
                        ? new Date(updated.lastHeartbeatAt).toISOString()
                        : null,
                    outcome,
                },
            });
        }
    }
    async function evaluateTimerPreflight(agent) {
        const pendingWakeup = await db
            .select({ id: agentWakeupRequests.id })
            .from(agentWakeupRequests)
            .where(and(eq(agentWakeupRequests.orgId, agent.orgId), eq(agentWakeupRequests.agentId, agent.id), inArray(agentWakeupRequests.status, ["queued", "deferred_issue_execution"]), sql `${agentWakeupRequests.runId} is null`, lte(agentWakeupRequests.requestedAt, new Date())))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        if (pendingWakeup) {
            return { shouldRun: false, skipReason: "heartbeat.preflight.pending_wakeup_request" };
        }
        const assigneeIssue = await db
            .select({ id: issues.id })
            .from(issues)
            .where(and(eq(issues.orgId, agent.orgId), eq(issues.assigneeAgentId, agent.id), inArray(issues.status, ["todo", "in_progress", "blocked"]), sql `${issues.hiddenAt} is null`))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        if (assigneeIssue) {
            return { shouldRun: true, reason: "assignee_issue" };
        }
        const reviewerIssue = await db
            .select({ id: issues.id })
            .from(issues)
            .where(and(eq(issues.orgId, agent.orgId), eq(issues.reviewerAgentId, agent.id), inArray(issues.status, ["in_review", "blocked"]), sql `${issues.hiddenAt} is null`, sql `
            NOT (
              ${issues.status} = 'blocked'
              AND EXISTS (
                SELECT 1
                FROM activity_log confirmed_blocked_review
                WHERE confirmed_blocked_review.org_id = ${agent.orgId}
                  AND confirmed_blocked_review.entity_type = 'issue'
                  AND confirmed_blocked_review.entity_id = ${issues.id}::text
                  AND confirmed_blocked_review.action = 'issue.review_decision_recorded'
                  AND confirmed_blocked_review.actor_type = 'agent'
                  AND confirmed_blocked_review.actor_id = ${agent.id}::text
                  AND confirmed_blocked_review.details ->> 'decision' = 'blocked'
                  AND confirmed_blocked_review.created_at >= COALESCE((
                    SELECT MAX(material_activity.created_at)
                    FROM activity_log material_activity
                    WHERE material_activity.org_id = ${agent.orgId}
                      AND material_activity.entity_type = 'issue'
                      AND material_activity.entity_id = ${issues.id}::text
                      AND (
                        (
                          material_activity.action = 'issue.updated'
                          AND jsonb_typeof(material_activity.details) = 'object'
                          AND EXISTS (
                            SELECT 1
                            FROM jsonb_object_keys(material_activity.details) AS detail_key(key)
                            WHERE detail_key.key NOT IN (
                              'identifier',
                              'issueIdentifier',
                              '_previous',
                              'source',
                              'reopened',
                              'reopenedFrom',
                              'normalizedFromStatus',
                              'normalizedReason'
                            )
                          )
                        )
                        OR (
                          material_activity.action = 'issue.comment_added'
                          AND NOT (
                            material_activity.actor_type = 'agent'
                            AND material_activity.actor_id = ${agent.id}::text
                          )
                        )
                      )
                  ), to_timestamp(0))
              )
            )
          `))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        if (reviewerIssue) {
            return { shouldRun: true, reason: "reviewer_issue" };
        }
        return { shouldRun: false, skipReason: "heartbeat.preflight.no_actionable_work" };
    }
    async function runHasIssueClosureComment(tx, run, issueId) {
        const commentActivity = await tx
            .select({ id: activityLog.id })
            .from(activityLog)
            .where(and(eq(activityLog.orgId, run.orgId), eq(activityLog.action, "issue.comment_added"), eq(activityLog.entityType, "issue"), eq(activityLog.entityId, issueId), eq(activityLog.runId, run.id)))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        return Boolean(commentActivity);
    }
    async function runHasIssueReviewDecision(tx, run, issueId) {
        const decisionActivity = await tx
            .select({ id: activityLog.id })
            .from(activityLog)
            .where(and(eq(activityLog.orgId, run.orgId), eq(activityLog.action, "issue.review_decision_recorded"), eq(activityLog.entityType, "issue"), eq(activityLog.entityId, issueId), eq(activityLog.runId, run.id)))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        return Boolean(decisionActivity);
    }
    async function issueHasDeferredWake(tx, orgId, issueId) {
        const deferred = await tx
            .select({ id: agentWakeupRequests.id })
            .from(agentWakeupRequests)
            .where(and(eq(agentWakeupRequests.orgId, orgId), eq(agentWakeupRequests.status, "deferred_issue_execution"), sql `${agentWakeupRequests.payload} ->> 'issueId' = ${issueId}`))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        return Boolean(deferred);
    }
    async function passiveFollowupAlreadyRecorded(tx, runId) {
        const idempotencyKey = `${ISSUE_PASSIVE_FOLLOWUP_REASON}:${runId}`;
        const existingWake = await tx
            .select({ id: agentWakeupRequests.id })
            .from(agentWakeupRequests)
            .where(eq(agentWakeupRequests.idempotencyKey, idempotencyKey))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        if (existingWake)
            return true;
        const existingReview = await tx
            .select({ id: activityLog.id })
            .from(activityLog)
            .where(and(eq(activityLog.runId, runId), inArray(activityLog.action, [
            "issue.closure_needs_operator_review",
            "issue.convergence_review_requested",
        ])))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        return Boolean(existingReview);
    }
    async function reviewerCloseoutAlreadyRecorded(tx, runId) {
        const existingWake = await tx
            .select({ id: agentWakeupRequests.id })
            .from(agentWakeupRequests)
            .where(eq(agentWakeupRequests.idempotencyKey, `${ISSUE_REVIEW_CLOSEOUT_REASON}:${runId}`))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        if (existingWake)
            return true;
        const existingReview = await tx
            .select({ id: activityLog.id })
            .from(activityLog)
            .where(and(eq(activityLog.runId, runId), eq(activityLog.action, "issue.review_closure_needs_operator_review")))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        return Boolean(existingReview);
    }
    async function issueHasConfirmedBlockedReviewerHandoff(tx, issue, reviewerAgentId) {
        if (issue.status !== "blocked")
            return false;
        const existingHandoff = await tx
            .select({ id: activityLog.id })
            .from(activityLog)
            .where(and(eq(activityLog.orgId, issue.orgId), eq(activityLog.action, "issue.review_decision_recorded"), eq(activityLog.entityType, "issue"), sql `${activityLog.entityId} = ${issue.id}::text`, eq(activityLog.actorType, "agent"), sql `${activityLog.actorId} = ${reviewerAgentId}::text`, sql `${activityLog.details} ->> 'decision' = 'blocked'`, sql `${activityLog.createdAt} >= COALESCE((
            SELECT MAX(material_activity.created_at)
            FROM activity_log material_activity
            WHERE material_activity.org_id = ${issue.orgId}
              AND material_activity.entity_type = 'issue'
              AND material_activity.entity_id = ${issue.id}::text
              AND (
                (
                  material_activity.action = 'issue.updated'
                  AND jsonb_typeof(material_activity.details) = 'object'
                  AND EXISTS (
                    SELECT 1
                    FROM jsonb_object_keys(material_activity.details) AS detail_key(key)
                    WHERE detail_key.key NOT IN (
                      'identifier',
                      'issueIdentifier',
                      '_previous',
                      'source',
                      'reopened',
                      'reopenedFrom',
                      'normalizedFromStatus',
                      'normalizedReason'
                    )
                  )
                )
                OR (
                  material_activity.action = 'issue.comment_added'
                  AND NOT (
                    material_activity.actor_type = 'agent'
                    AND material_activity.actor_id = ${reviewerAgentId}::text
                  )
                )
              )
          ), to_timestamp(0))`))
            .limit(1)
            .then((rows) => rows[0] ?? null);
        return Boolean(existingHandoff);
    }
    async function evaluatePassiveIssueClosureForLockedIssue(input) {
        const { tx, run, issue, now } = input;
        const context = parseObject(run.contextSnapshot);
        const runIssueId = readNonEmptyString(context.issueId);
        if (!runIssueId || runIssueId !== issue.id)
            return { kind: "none", reason: "run_not_issue_backed" };
        if (run.status !== "succeeded")
            return { kind: "none", reason: "run_not_successful" };
        const reviewerRun = (issue.status === "in_review" || issue.status === "blocked") &&
            issue.reviewerAgentId === run.agentId &&
            (run.invocationSource === "review" ||
                readNonEmptyString(context.role) === "reviewer" ||
                readNonEmptyString(context.wakeSource) === "review");
        if (reviewerRun) {
            if (await runHasIssueReviewDecision(tx, run, issue.id)) {
                return { kind: "none", reason: "review_decision_recorded" };
            }
            if (await issueHasConfirmedBlockedReviewerHandoff(tx, issue, run.agentId)) {
                return { kind: "none", reason: "blocked_reviewer_handoff_confirmed" };
            }
            if (await issueHasDeferredWake(tx, issue.orgId, issue.id)) {
                return { kind: "none", reason: "deferred_issue_wake_exists" };
            }
            if (await reviewerCloseoutAlreadyRecorded(tx, run.id)) {
                return { kind: "none", reason: "reviewer_closeout_already_recorded" };
            }
            const reviewCloseout = normalizeReviewCloseoutContext(context.reviewCloseout);
            const currentAttempt = reviewCloseout?.attempt ?? 0;
            const maxAttempts = reviewCloseout?.maxAttempts ?? ISSUE_REVIEW_CLOSEOUT_MAX_ATTEMPTS;
            const originRunId = reviewCloseout?.originRunId ?? run.id;
            if (currentAttempt >= maxAttempts) {
                return {
                    kind: "reviewer_closeout_operator_review",
                    issue,
                    originRunId,
                    previousRunId: run.id,
                    attempts: currentAttempt,
                    maxAttempts,
                    reason: ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON,
                };
            }
            return {
                kind: "reviewer_closeout",
                issue,
                originRunId,
                previousRunId: run.id,
                attempts: currentAttempt + 1,
                maxAttempts,
                reason: ISSUE_REVIEW_CLOSEOUT_FAILURE_REASON,
            };
        }
        if (issue.status !== "todo" && issue.status !== "in_progress") {
            return { kind: "none", reason: "issue_has_closure_status" };
        }
        if (issue.assigneeAgentId !== run.agentId) {
            return { kind: "none", reason: "issue_no_longer_assigned_to_run_agent" };
        }
        if (!issueHasReviewer(issue) && await runHasIssueClosureComment(tx, run, issue.id)) {
            return { kind: "none", reason: "run_authored_issue_comment" };
        }
        if (await issueHasDeferredWake(tx, issue.orgId, issue.id)) {
            return { kind: "none", reason: "deferred_issue_wake_exists" };
        }
        if (await passiveFollowupAlreadyRecorded(tx, run.id)) {
            return { kind: "none", reason: "passive_followup_already_recorded" };
        }
        const agent = await tx
            .select()
            .from(agents)
            .where(eq(agents.id, run.agentId))
            .then((rows) => rows[0] ?? null);
        if (!agent || agent.orgId !== run.orgId) {
            return { kind: "none", reason: "agent_not_found" };
        }
        const policy = parseHeartbeatPolicy(agent);
        if (hasCredibleTimerContinuation({ agent, policy, run, now })) {
            return { kind: "none", reason: "timer_continuity_expected" };
        }
        const passiveContext = normalizePassiveFollowupContext(context.passiveFollowup);
        const currentAttempt = passiveContext?.attempt ?? 0;
        const originRunId = passiveContext?.originRunId ?? run.id;
        if (currentAttempt >= ISSUE_PASSIVE_FOLLOWUP_MAX_ATTEMPTS) {
            if (issueHasReviewer(issue)) {
                return {
                    kind: "reviewer_convergence",
                    issue,
                    originRunId,
                    previousRunId: run.id,
                    attempts: currentAttempt,
                    reason: ISSUE_PASSIVE_FOLLOWUP_FAILURE_REASON,
                };
            }
            return {
                kind: "operator_review",
                issue,
                originRunId,
                previousRunId: run.id,
                attempts: currentAttempt,
                reason: ISSUE_PASSIVE_FOLLOWUP_FAILURE_REASON,
            };
        }
        const nextAttempt = currentAttempt + 1;
        const requestedAt = new Date(now.getTime() + passiveFollowupCooldownMs(nextAttempt));
        const contextSnapshot = buildPassiveFollowupContextSnapshot({
            run,
            issue,
            originRunId,
            attempt: nextAttempt,
            now,
        });
        const taskKey = deriveTaskKey(contextSnapshot, { issueId: issue.id });
        const sessionBefore = await resolveSessionBeforeForWakeup(agent, taskKey);
        const requestPayload = {
            issueId: issue.id,
            originRunId,
            previousRunId: run.id,
            attempt: nextAttempt,
            reason: ISSUE_PASSIVE_FOLLOWUP_FAILURE_REASON,
        };
        const wakeupRequest = await tx
            .insert(agentWakeupRequests)
            .values({
            orgId: run.orgId,
            agentId: run.agentId,
            source: "automation",
            triggerDetail: "system",
            reason: ISSUE_PASSIVE_FOLLOWUP_REASON,
            payload: requestPayload,
            status: "queued",
            requestedByActorType: "system",
            requestedByActorId: "issue_closure_governance",
            idempotencyKey: `${ISSUE_PASSIVE_FOLLOWUP_REASON}:${run.id}`,
            requestedAt,
            updatedAt: now,
        })
            .returning()
            .then((rows) => rows[0]);
        const followupRun = await tx
            .insert(heartbeatRuns)
            .values({
            orgId: run.orgId,
            agentId: run.agentId,
            invocationSource: "automation",
            triggerDetail: "system",
            status: "queued",
            wakeupRequestId: wakeupRequest.id,
            contextSnapshot,
            sessionIdBefore: sessionBefore,
            updatedAt: now,
        })
            .returning()
            .then((rows) => rows[0]);
        await tx
            .update(agentWakeupRequests)
            .set({
            runId: followupRun.id,
            updatedAt: now,
        })
            .where(eq(agentWakeupRequests.id, wakeupRequest.id));
        await tx
            .update(issues)
            .set({
            executionRunId: followupRun.id,
            executionAgentNameKey: normalizeAgentNameKey(agent.name),
            executionLockedAt: now,
            updatedAt: now,
        })
            .where(eq(issues.id, issue.id));
        return {
            kind: "queued",
            run: followupRun,
            issue,
            originRunId,
            previousRunId: run.id,
            attempt: nextAttempt,
            requestedAt,
        };
    }
    return { enqueueRecoveryRun, enqueueProcessLossRetry, parseHeartbeatPolicy, markAgentHeartbeatChecked, evaluateTimerPreflight, runHasIssueClosureComment, runHasIssueReviewDecision, issueHasDeferredWake, passiveFollowupAlreadyRecorded, reviewerCloseoutAlreadyRecorded, issueHasConfirmedBlockedReviewerHandoff, evaluatePassiveIssueClosureForLockedIssue };
}
//# sourceMappingURL=heartbeat.recovery.js.map